.NET8在.NET7的基础上进行了进一步的优化,比如CHRL(全称:CORINFO_HELP_RNGCHKFAIL)优化技术,CORINFO_HELP_RNGCHKFAIL是边界检查,在.NET7里面它已经进行了部分优化,但是.NET8里面它继续优化,类似人工智能,.NET8能意识到某些性能问题,从而进行优化。本篇来看下。
public class Tests { private byte[] _array = new byte[8]; private int _index = 4; // 堆代码 duidaima.com 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+索引)取其值返回即可。所以这个地方有清晰可见的优化的地方。
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虽然也是骚操,但是它这个骚操不够彻底。
; 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。所以不需要边界检查,全给它优化掉了。