.Net8通过各种骚操,把性能提升到了前所未有的高度。超越以往任何版本,也涵盖了后续版本,比如.NET9或许可能没有如此大的性能优化了。本篇来看下它其中的一个优化:类型转换的优化效果。
通过类型检查的优化,优化掉某些情况下类型转换的时候JIT类型检查的函数。下面的代码是类型检查的典型应用。
[HideColumns("Error","StdDev","Median","RatioSD")][DisassemblyDiagnoser(maxDepth:0)]publicclass Tests { private readonly string[]_strings=new string[1];[Benchmark]publicstring Get1()=>_strings[0];[Benchmark]publicstring Get2()=>Volatile.Read(ref _strings[0]);}publicpartialclass Program { static void Main(string[]args){ BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);} }
我们看到_strings是个私有数组,Get1函数中获取_strings数组的第一个值。所以它是直接用ldelem.ref IL执行即可
ldelem.ref
但是Get2里面对数组元素进行了引用,所以Roslyn的指令是:
ldelema[System.Runtime]System.String
如果ref类型的变量,被赋值为不同于这个变量的类型则会违反类型安全性。通常情况下ldelema需要进行类型检查,也就是用JIT辅助函数CORINFO_HELP_LDELEMA_REF来进行检查,以确保不会违反类型安全性。
这个安全性的检查会极大损耗性能,.NET8的JIT进行了一个优化,思路是如果是sealed关键字标记的类型,就不会进行安全性检查,这样就会提高性能。为什么sealed不会呢?
这其实是利用了它的一个特性,就是不会被继承。不会被继承,就不会被子类的类型所困扰,只有string一个类型,自然不会用以进行类型检查了。
这是第一点优化,下面看下。
优化了类型安全检查,缩短了编译时间,提高了性能。来看下.Net7和.NET8的生成Get2函数的的不同点
.Net7:
Tests.Get2()sub rsp,28mov rcx,[rcx+8]xoredx,edx mov r8,offsetMT_System.StringcallCORINFO_HELP_LDELEMA_REF mov rax,[rax]addrsp,28ret;Total bytesofcode33
.Net7它这里有一个CORINFO_HELP_LDELEMA_REF进行安全性检查。
.Net8:
;Tests.Get2()sub rsp,28mov rax,[rcx+8]cmp dword ptr[rax+8],0jbe short M00_L00 mov rax,[rax+10]addrsp,28ret M00_L00:callCORINFO_HELP_RNGCHKFAILint3;Total bytesofcode29
.Net8里它没有了CORINFO_HELP_LDELEMA_REF
因为string类型是sealed,它的原型如下:
publicsealed class String : IEnumerable<char>,IEnumerable,ICloneable,IComparable,IComparable<String?>,IConvertible,IEquatable<String?>{//这里代码省略}
JIT会判断类型是否是sealed标志,如果是则不进行安全性检查优化。
虽然.Net8去掉了CORINFO_HELP_LDELEMA_REF,
但是多了范围的检查CORINFO_HELP_RNGCHKFAIL,那它这个性能如何呢?
我们来测试下:
dotnet run-cRelease-f net7.0--filter "*" --runtimes net7.0 net8.0
结果是:
Method | Runtime | Mean | Ratio | Code Size |
Get2 | .NET 7.0 | 1.0537 ns | 1.00 | 33 B |
Get2 | .NET 8.0 | 0.2423 ns | 0.23 | 29 B |
我们看到同样代码,.Net8里面比.Net7的性能提升了5倍之多。
承接上面,上面sealed去掉了类型检查。
然后在类型转换的时候,一般的类型转换JIT使用的是CastHelpers.ChkCastAny来进行。
但是.Net8里面内联了一个方法
用以缩短CastHelpers.ChkCastAny的编译时间,提高编译的时间和程序的性能。
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem.Runtime.CompilerServices;BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);[HideColumns("Error","StdDev","Median","RatioSD")]publicclass Tests { private readonly object _o="hello";[Benchmark]publicstring GetString()=>Cast<string>(_o);[MethodImpl(MethodImplOptions.NoInlining)]publicT Cast<T>(object o)=>(T)o;}
同样的
dotnet run-cRelease-f net7.0--filter "*" --runtimes net7.0 net8.0
结果如下:
Method | Runtime | Mean | Ratio |
GetString | .NET 7.0 | 3.018 ns | 1.00 |
GetString | .NET 8.0 | 1.198 ns | 0.40 |
.Net8是三倍于.Net7的运行速度。去掉类型检查+类型转换的内联,大幅度的提升效率,可见.Net8的性能优化确实不容小觑。
参考如下:
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/
最后推荐下个人的CLR/JIT交流圈,里面有多篇个人编写的高质量的原创栏目和文章。学习心得,项目经验等。带你进入.Net核心技术阶层,脱离curd工程师范畴。