前言
本篇看下.NET9 PreView6 JIT的第二个性能优化更新,循环优化和地址暴露方面的改进。
循环优化
一般循环都是通过for来进行递增判断,如下:
代码语言:javascript复制for (int i = 0; i < 100; i )
{
Foo();
}
但是在一些其它架构上,比如Arm/Risc-V上,递减循环可能更具性能提升价值
代码语言:javascript复制for (int i = 100; i > 0; i--)
{
Foo();
}
减少指令集,始终是优化的一个方向。第一个例子:
代码语言:javascript复制G_M35517_IG02: ;; offset=0x0005
xor ebx, ebx // Set i to 0
G_M35517_IG03: ;; offset=0x0007
call [Foo()]
inc ebx // Increment i
cmp ebx, 100 // Compare i to 100
jl SHORT G_M35517_IG03 // Continue loop if (i < 100)
第二个例子
代码语言:javascript复制G_M35517_IG02: ;; offset=0x0005
mov ebx, 100 // Set i to 100
G_M35517_IG03: ;; offset=0x000A
call [Foo()]
dec ebx // Decrement i
jne SHORT G_M35517_IG03 // Continue loop if (i != 0)
代码的大小减少很小,但如果循环运行了大量迭代,则性能改进可能很大。RyuJIT 现在可以识别何时可以在不影响程序行为的情况下翻转循环计数器变量的方向(意即,X64上当JIT识别到不影响程序逻辑的情况下,循环递增把它改变成循环递减至0,从而减少指令单生成,提升性能),并进行转换。
地址暴露
思考一个问题,将一个方法里面的变量传递给另外一个方法,这里的变量传递过程中实际上是传递的是变量的地址。为了确保这个地址里面的值不会在传递的时候,被CLR/JIT的某个功能或者特性修改了导致出错,所以需要额外的功能做这件事情。
问题来了,这个额外的功能,会经常性的抑制一些JIT里面的优化,导致性能上不去。怎么办呢?那就是减少局部变量地址暴露的可能性。
代码语言:javascript复制public struct Awaitable
{
public int Opts;
public Awaitable(bool value)
{
Opts = value ? 1 : 2;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Test() => new Awaitable(false).Opts;
x64
代码语言:javascript复制G_M59043_IG01: ;; offset=0x0000
push rax
G_M59043_IG02: ;; offset=0x0001
xor eax, eax
mov dword ptr [rsp], eax
mov dword ptr [rsp], 2 // Set Opts to 2
mov eax, dword ptr [rsp] // Return Opts
G_M59043_IG03: ;; offset=0x0010
add rsp, 8
ret
; Total bytes of code: 21
因为JIT实际上预先判断,已经知道了Awaitable方法的参数value为false。所以传递Opts地址时候,不需要保持其值不变。同时也避免了抑制性能的行为,即PreView6只需要赋值Opts=2即可
代码语言:javascript复制G_M59043_IG02: ;; offset=0x0000
mov eax, 2 // Return 2
G_M59043_IG03: ;; offset=0x0005
ret