背景
手头上有个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。
考虑迁移,暂且忍受这块的性能损耗。期待未来版本修复。

博主在将C++/CLI项目从.NET Framework 4.8迁移至.NET Core时,发现.NET 5.0的Marshal::PtrToStructure方法性能显著下降,比.NET Core 3.1慢3-10倍。经过测试和研究,确认这是.NET 5.0已知的问题,并未在后续版本中得到解决。最终,博主选择迁移到.NET Core 3.1,等待更好的LTS版本。
906

被折叠的 条评论
为什么被折叠?



