【翻译】.NET 5中的性能改进 (9)


这里提到的大多数改进都集中在吞吐量上,JIT产生的代码执行得更快,而更快的代码通常(尽管不总是)更小。从事JIT工作的人们实际上非常关注代码大小,在许多情况下,将其作为判断更改是否有益的主要指标。更小的代码并不总是更快的代码(可以是相同大小的指令,但开销不同),但从高层次上来说,这是一个合理的度量,更小的代码确实有直接的好处,比如对指令缓存的影响更小,需要加载的代码更少,等等。在某些情况下,更改完全集中在减少代码大小上,比如在出现不必要的重复的情况下。考虑一下这个简单的基准:

private int _offset = 0; [Benchmark] public int Throw helpers() { var arr = new int[10]; var s0 = new Span<int>(arr, _offset, 1); var s1 = new Span<int>(arr, _offset + 1, 1); var s2 = new Span<int>(arr, _offset + 2, 1); var s3 = new Span<int>(arr, _offset + 3, 1); var s4 = new Span<int>(arr, _offset + 4, 1); var s5 = new Span<int>(arr, _offset + 5, 1); return s0[0] + s1[0] + s2[0] + s3[0] + s4[0] + s5[0]; }


Span构造函数,,T是一个值类型时,结果有两个叫网站ThrowHelper类上的方法,一个扔一个失败的null检查时抛出的输入数组和一个偏移量和计数的范围(像ThrowArgumentNullException ThrowHelper包含non-inlinable方法,其中包含实际的扔,避免了相关代码大小在每个调用网站;JIT目前还不能“outlining”(与“inlining”相反),因此需要在重要的情况下手工完成)。在上面的示例中,我们创建了6个Span,这意味着对Span构造函数的6次调用,所有这些调用都将内联。JIT数组为空,所以它可以消除零检查和ThrowArgumentNullException内联代码,但是它不知道是否偏移量和计算范围内,因此它需要保留ThrowHelper范围检查和调用站点。ThrowArgumentOutOfRangeException方法。在.NET Core 3.1中,这个Throw helpers方法生成了如下代码:

M00_L00: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L01: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L02: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L03: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L04: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L05: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3


在.NET 5中,感谢dotnet/coreclr#27113, JIT能够识别这种重复,而不是所有的6个呼叫站点,它将最终合并成一个:

M00_L00: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3

所有失败的检查都跳到这个共享位置,而不是每个都有自己的副本

Method Runtime Code Size
Throw helpers   .NET FW 4.8   424 B  
Throw helpers   .NET Core 3.1   252 B  
Throw helpers   .NET 5.0   222 B  


这些只是.NET 5中对JIT进行的众多改进中的一部分。还有许多其他改进。dotnet/runtime#32368导致JIT将数组的长度视为无符号,这使得JIT能够对在长度上执行的某些数学运算(例如除法)使用更好的指令。 dotnet/runtime#25458 使JIT可以对某些无符号整数运算使用更快的基于0的比较。 当开发人员实际编写> = 1时,使用等于!= 0的值。dotnet/runtime#1378允许JIT将“ constantString” .Length识别为常量值。 dotnet/runtime#26740 通过删除nop填充来减小ReadyToRun图像的大小。 dotnet/runtime#330234使用加法而不是乘法来优化当x为浮点数或双精度数时执行x * 2时生成的指令。dotnet/runtime#27060改进了为Math.FusedMultiplyAdd内部函数生成的代码。 dotnet/runtime#27384通过使用比以前更好的篱笆指令使ARM64上的易失性操作便宜,并且dotnet/runtime#38179在ARM64上执行窥视孔优化以删除大量冗余mov指令。 等等。


JIT中还有一些默认禁用的重要更改,目的是获得关于它们的真实反馈,并能够在默认情况下post-启用它们。净5。例如,dotnet/runtime#32969提供了“On Stack Replacement”(OSR)的初始实现。我在前面提到了分层编译,它使JIT能够首先为一个方法生成优化最少的代码,然后当该方法被证明是重要的时,用更多的优化重新编译该方法。这允许代码运行得更快,并且只有在运行时才升级有效的方法,从而实现更快的启动时间。但是,分层编译依赖于替换实现的能力,下次调用它时,将调用新的实现。但是长时间运行的方法呢?对于包含循环(或者,更具体地说,向后分支)的方法,分层编译在默认情况下是禁用的,因为它们可能会运行很长时间,以至于无法及时使用替换。OSR允许方法在执行代码时被更新,而它们是“在堆栈上”的;PR中包含的设计文档中有很多细节(也与分层编译有关,dotnet/runtime#1457改进了调用计数机制,分层编译通过这种机制决定哪些方法应该重新编译以及何时重新编译)。您可以通过将COMPlus_TC_QuickJitForLoops和COMPlus_TC_OnStackReplacement环境变量设置为1来试验OSR。另一个例子是,dotnet/runtime#1180 改进了try块内代码的生成代码质量,使JIT能够在寄存器中保存以前不能保存的值。您可以通过将COMPlus_EnableEHWriteThr环境变量设置为1来进行试验。


还有一堆等待拉请求JIT尚未合并,但很可能在.NET 5发布(除此之外,我预计还有更多在.NET 5发布之前还没有发布的内容)。例如,dotnet/runtime#32716允许JIT替换一些分支比较,如a == 42 ?3: 2无分支实现,当硬件无法正确预测将采用哪个分支时,可以帮助提高性能。或dotnet/runtime#37226,它允许JIT采用像“hello”[0]这样的模式并将其替换为h;虽然开发人员通常不编写这样的代码,但在涉及内联时,这可以提供帮助,通过将常量字符串传递给内联的方法,并将其索引到常量位置(通常在长度检查之后,由于dotnet/runtime#1378,长度检查也可以成为常量)。或dotnet/runtime#1224,它改进了Bmi2的代码生成。MultiplyNoFlags内在。或者dotnet/runtime#37836,它将转换位操作。将PopCount转换为一个内因,使JIT能够识别何时使用常量参数调用它,并将整个操作替换为一个预先计算的常量。或dotnet/runtime#37254,它删除使用const字符串时发出的空检查。或者来自@damageboy的dotnet/runtime#32000 ,它优化了双重否定。

Intrinsics

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsssyf.html