背景
手头上有个C++/CLI
项目,主要是用来封装C++
的dll,方便.NET调用的。之前是在.NET FW 4.8 runtime
上,最近想迁移至.NET Core runtime
。当前的时间点有两个选择,一个是.NET Core 3.1
,一个是.NET 5.0
。一个是LTS
版,一个是新鲜出炉版。没有多想,觉得.NET 5.0
虽然很新,有很多坑不稳定,但不至于被我踩上,加上网上吹的那么多性能提升,首选了升级至.NET 5.0
。
C++/CLI项目的migrate
C++/CLI
项目从.NET FW 迁移至.NET改动不大,操作如下:
主要就是选择新版的rumtime
,由/clr
更改为/clr:netcore
。调整对应的.NET Traget Framework Version
。v4.8
-> .NET Core 3.1 or .NET 5.0
。
分析
由于是个C++/CLI
项目,对性能还是比较敏感的,虽然网上有很多性能测试的文章,但少有C++/CLI项目在不同runtime
下的测试对比。
测试代码在C++/CLI
项目中,运行耗时计算使用<chrono>
。
// 示例伪代码
#include <chrono>
auto tp1 = chrono::high_resolution_clock::now();
// code
auto tp2 = chrono::high_resolution_clock::now();
auto duration = chrono::duration_cast<chrono::microseconds>(tp2 - tp1).count();
C++11 chrono头文件提供了3个标准时钟可以用来计时:
- system_clock - 系统提供的实时时钟
- high_resolution_clock - 当前系统时钟周期最短的时钟
- steady_clock - 不会被调整的单调时钟
测出来的结果让我大跌眼镜。针对C++/CLI
项目代码,.NET FW 4.8
的性能大概是.NET 5.0
的3-10倍(测得比较随意,都是些简单代码重复执行)。一开始我以为是迁移后的C++/CLI
项目需要其他设置,所以花了一天时间各种配置调优,结果变化不大。
我尝试一行行代码做性能对比,发现性能差异主要提现在Marshal::StructureToPtr
和Marshal::PtrToStructure
这两个方法上。
就在我心灰意冷,准备回到.NET FW 4.8
怀抱的时候,我查到了这个。
Github dotnet 5.0 Marshal.PtrToStructure is slower 8x than netcore3.1 #45100
截止到2021-12-08,.NET6已经发布,这个issue还没有被解决。这个已经被推迟到.NET7的进度里了。而让人难过的是,.NET8才是LTS。
这个github的issue
和的我情况差不多,虽然他并不是C++/CLI
项目,但也能说明一定的问题。即.NET 5.0
里的Marshal::PtrToStructure
性能有问题,至少比.NET Core 3.1
差了很多。
受到启发,我把C++/CLI
项目runtime
改成了.NET Core 3.1
并与.NET FW 4.8
做比较测试。测下来,基本上性能差不多,.NET Core 3.1
比.NET FW 4.8
有略微的优势。至少这给了我信心,可以暂时先迁往3.1
,等时机成熟,再前往LTS
版的.NET 6.0
。
总结
本篇文章遇到的问题,严格意义上,不能算是C++/CLI
项目的问题,但由于C++/CLI
项目用到Marshal::StructureToPtr
和Marshal::PtrToStructure
的场景比较多,所以受到了影响。但寻根溯源,这还是.NET 5.0
的问题。毕竟是新版本,一不小心就踩坑翻车了。本文也是提醒大家,如果需要在.NET 5.0
中使用Marshal::StructureToPtr
和Marshal::PtrToStructure
等方法,务必小心谨慎。
更新
20230803
针对Marshal::PtrToStructure
这个操作,主要用于将native
类型转化为managed
类型,循环操作10000次。
运行版本 | dll编译版本 | 平均单次耗时 |
---|---|---|
nfx 4.8 | nfx 4.8 | 约1.28us-1.35us |
netcore 3.1 | netcore 3.1 | 约1.13us-1.162us |
net 6.0 | netcore 3.1 | 约4.50us-4.89us |
net 7.0 | netcore 3.1 | 约5.13us-7.27us |
netcore 3.1 | net 6.0 | 不支持 |
net 6.0 | net 6.0 | 约4.58us-4.99us |
net 7.0 | net 6.0 | 约5.44us-5.86us |
netcore 3.1 | net 7.0 | 不支持 |
net 6.0 | net 7.0 | 不支持 |
net 7.0 | net 7.0 | 约5.41us-6.11us |
由以上粗略的测试可知,Marshal::PtrToStructure
的性能,在nfx4.8
与netcore3.1
上基本一致。随着.NET
版本提升,性能持续下降。在net7.0
上降低比较显著。考虑到该版本为过渡版本,可以忽略。该问题在GitHub上的相关issue仍然没有解决。考虑到net
和nfx
版本逐渐脱节,且C#新版本特性很吸引人。nfx4.8
目前只支持到C#8.0
,net6.0
支持最新的C#11
。
考虑迁移,暂且忍受这块的性能损耗。期待未来版本修复。