前言
.NET8
在 .NET7
的基础上进行了进一步的优化,比如 CHRL
(全称:CORINFO_HELP_RNGCHKFAIL
)优化技术,CORINFO_HELP_RNGCHKFAIL
是边界检查,在 .NET7
里面它已经进行了部分优化,但是 .NET8
里面它继续优化,类似人工智能,.NET8
能意识到某些性能问题,从而进行优化。
概述
JIT
会对数组,字符串的范围边界进行检查。比如数组的索引是否在数组长度范围内,不能超过。所以 JIT
就会产生边界检查的步骤。
public class Tests
{
private byte[] _array = new byte[8];
private int _index = 4;
public void Get() => Get(_array, _index);
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte Get(byte[] array, int index) => array[index];
}
Get
函数 .NET7
的 ASM
如下:
; Tests.Get(Byte[], Int32)
sub rsp,28
cmp edx,[rcx+8]
jae short M01_L00
mov eax,edx
movzx eax,byte ptr [rcx+rax+10]
add rsp,28
ret
M01_L00:
call CORINFO_HELP_RNGCHKFAIL
int 3
cmp
指令把数组的 MT
(方法表) 偏移 8
位置的数组长度与当前的数组索引对比,两者如果索引大于(后者)或等于(jae
)数组长度(前者)的时候。就会跳转到 CORINFO_HELP_RNGCHKFAIL
进行边界检查,可能会引发超出引范围的异常 IndexOutOfRangeException
。但是实际上这段这段代码的访问只需要两个 mov
,一个是数组的索引,一个是(MT+0x10+索引
)取其值返回即可。所以这个地方有清晰可见的优化的地方。
.NET8
学习了一些范围边界的智能化优化,也就说,有的地方不需要边界检查,从而把边界检查优化掉,用以提高代码的性能。下面例子:
private readonly int[] _array = new int[7];
public int GetBucket() => GetBucket(_array, 42);
private static int GetBucket(int[] buckets, int hashcode) =>
buckets[(uint)hashcode % buckets.Length];
.NET7
它的 ASM
如下:
; Tests.GetBucket()
sub rsp,28
mov rcx,[rcx+8]
mov eax,2A
mov edx,[rcx+8]
mov r8d,edx
xor edx,edx
idiv r8
cmp rdx,r8
jae short M00_L00
mov eax,[rcx+rdx*4+10]
add rsp,28
ret
M00_L00:
call CORINFO_HELP_RNGCHKFAIL
int 3
它依然进行了边界检查,然 .NET8
的 JIT
能自动识别到 (uint)hashcode%buckets.Length
这个索引不可能超过数组的长度也就是 buckets.Length
。所以 .NET8
可以省略掉边界检查,如下 .NET8 ASM
:
; Tests.GetBucket()
mov rcx,[rcx+8]
mov eax,2A
mov r8d,[rcx+8]
xor edx,edx
div r8
mov eax,[rcx+rdx*4+10]
ret
再看下另外一个例子:
public class Tests
{
private readonly string _s = "\"Hello, World!\"";
public bool IsQuoted() => IsQuoted(_s);
private static bool IsQuoted(string s) =>
s.Length >= 2 && s[0] == '"' && s[^1] == '"';
}
IsQuoted
检查字符串是否至少有两个字符,并且字符串开头和结尾均以引号结束,s[^1]
表示 s[s.Length - 1]
也就是字符串的长度。
.NET7 ASM
如下:
; Tests.IsQuoted(System.String)
sub rsp,28
mov eax,[rcx+8]
cmp eax,2
jl short M01_L00
cmp word ptr [rcx+0C],22
jne short M01_L00
lea edx,[rax-1]
cmp edx,eax
jae short M01_L01
mov eax,edx
cmp word ptr [rcx+rax*2+0C],22
sete al
movzx eax,al
add rsp,28
ret
M01_L00:
xor eax,eax
add rsp,28
ret
M01_L01:
call CORINFO_HELP_RNGCHKFAIL
int 3
注意看 .NET7
的骚操,它实际上进行了边界检查,但是只检查了一个,因为它只有一个 jae
指令跳转。这是为什么呢?JIT
已经知道不需要对 s[0]
进行边界检查,因为 s.Length >= 2
已经检查过了,只要是小于2
的索引(因为索引是无符号,没有负数)都不需要检查。但是依然对 s[s.Length - 1]
进行了边界检查,所以 .NET7
虽然也是骚操,但是它这个骚操不够彻底。
我们来看下 .NET8
的骚操,.NET8 ASM
如下:
; Tests.IsQuoted(System.String)
mov eax,[rcx+8]
cmp eax,2
jl short M01_L00
cmp word ptr [rcx+0C],22
jne short M01_L00
dec eax
cmp word ptr [rcx+rax*2+0C],22
sete al
movzx eax,al
ret
M01_L00:
xor eax,eax
ret
完全没有了边界检查,JIT
不仅意识到 s[0]
是安全的,因为检查过了 s.Length >= 2
。因为检查过了 s.Length >= 2
,还意识到 s.length > s.Length-1 >=1
。所以不需要边界检查,全给它优化掉了。
可以看到 .NET8
的性能优化的极致有多厉害,它基本上榨干了 JIT
的引擎,让其进行最大智能化程度的优化。
转载声明: