.NET8極致性能優化CHRL

前言

.NET8在.NET7的基礎上進行了進一步的優化,比如CHRL(全稱:CORINFO_HELP_RNGCHKFAIL)優化技術,CORINFO_HELP_RNGCHKFAIL是邊界檢查,在.NET7裏面它已經進行了部分優化,但是.NET8裏面它繼續優化,類似人工智能,.NET8能意識到某些性能問題,從而進行優化。本篇來看下。原文:.NET8極致性能優化CHRL

概述

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

; 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的引擎,讓其進行最大智能化程度的優化。


點擊下加入技術討論羣:

歡迎加入.NET技術交流羣

結尾

作者:江湖評談
歡迎關注公衆號:jianghupt,文章首發,以及更多高階內容分享。
image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章