.Net8頂級技術:邊界檢查之IR解析(慎入)

前言

C#這種語言之所以號稱安全的,面向對象的語言。這個安全兩個字可不是瞎叫的哦。因爲JIT會檢查任何可能超出分配範圍的數值,以便使其保持在安全邊界內。這裏有兩個概念,其一邊界檢查,其二IR解析。後者的生成是前者的功能的保證。啥叫IR,你以爲的IL是中間語言,其實並不是,還有一層IR中間表象。.Net8的頂級技術之一(非專屬),曉者寥寥。本篇來看看這兩項技術。

概括

1.邊界檢查的缺陷
也叫循環提升,這裏邊界檢查以數組的邊界檢查爲例,看下C#代碼
C# Code


using System.Runtime.CompilerServices;
class Program
{
    static void Main()
    {
        int[] array = new int[10_000_000];
        for (int i = 0; i < 1_000_000; i++)
        {
            Test(array);
        }
    }
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static bool Test(int[] array)
    {
        for (int i = 0; i < 0x12345; i++)
        {
            if (array[i] == 42)
            {
                return true;
            }
        }
        return false;
    }
}  

JIT並不知道數組array[i]裏面的i索引是否超過了array數組的長度。所以每次循環都會檢查索引的大小,如果超過則報異常,不超過繼續循環,這種功能就叫做邊界檢查。是.Net6 JIT自動加上去的,但是它有缺陷。

缺陷就在於,每次循環都檢查,極大消耗了代碼的運行效率。爲了避免這種缺陷,是否可以在循環之前判斷array數組的長度小於或者循環的最大值。通過這種一次性的判斷,取代每次循環的判斷,最大化提升代碼運行效率。
在.Net8裏面這種情況是可行的。
.Net8 JIT Machine Code


G_M000_IG01:                ;; offset=0000H
       4883EC28             sub      rsp, 40
G_M000_IG02:                ;; offset=0004H
       33C0                 xor      eax, eax
       4885C9               test     rcx, rcx
       7429                 je       SHORT G_M000_IG05
       81790845230100       cmp      dword ptr [rcx+08H], 0x12345
       7C20                 jl       SHORT G_M000_IG05
       0F1F40000F1F840000000000 align    [12 bytes for IG03]
G_M000_IG03:                ;; offset=0020H
       8BD0                 mov      edx, eax
       837C91102A           cmp      dword ptr [rcx+4*rdx+10H], 42
       7429                 je       SHORT G_M000_IG08
       FFC0                 inc      eax
       3D45230100           cmp      eax, 0x12345
       7CEE                 jl       SHORT G_M000_IG03
G_M000_IG04:                ;; offset=0032H
      EB17                 jmp      SHORT G_M000_IG06
G_M000_IG05:                ;; offset=0034H
       3B4108               cmp      eax, dword ptr [rcx+08H]
       7323                 jae      SHORT G_M000_IG10
       8BD0                 mov      edx, eax
       837C91102A           cmp      dword ptr [rcx+4*rdx+10H], 42
       7410                 je       SHORT G_M000_IG08
       FFC0                 inc      eax
       3D45230100           cmp      eax, 0x12345
       7CE9                 jl       SHORT G_M000_IG05
G_M000_IG06:                ;; offset=004BH
       33C0                 xor      eax, eax
G_M000_IG07:                ;; offset=004DH
       4883C428             add      rsp, 40
       C3                   ret
G_M00_IG08:                ;; offset=0052H
       B801000000           mov      eax, 1
G_M000_IG09:                ;; offset=0057H
       4883C428             add      rsp, 40
       C3                   ret
G_M000_IG10:                ;; offset=005CH
       E89F82C25F           call     CORINFO_HELP_RNGCHKFAIL
       CC                   int3
; Total bytes of code 98

誠如上面所言,邊界檢查的判斷放在了for循環的外面。if和else分成快速和慢速路徑,前者進行了優化。逆向成C#代碼如下

if(array!=null && array.length >=0x12345)//數組不能爲空,且數組的長度不能小於循環的長度。否則可能邊界溢出
{
   for(int i=0;i<0x12345;i++)
   {
     if(array[i]==42)//這裏不再檢查邊界
     {
       return true;
     }
   }
   return false;
}
else
{
  for(int i=0;i<0x2345;i++)
  {
     if(array[i]==42)//邊界檢查
     return true;
  }
  return flase;
}

邊界檢查不是本節的重點,重點是這個邊界檢查是如何通過IR生成的,以及優化。因爲IL代碼裏面並沒有。

2.IR的生成
部分代碼。常規的認爲,C#的運行過程是:
C# Code->
IL ->
Machine Code
一般的認爲,IL是中間語言,或者字節碼。但是實際上還有一層在JIT裏面。如下:
C# Code ->
IL ->
IR ->
Machine Code
這個IR是對IL進行各種騷操作。最重要的一點就是各種優化和變形。這裏來看看IR是如何對IL進行邊界檢查優化的。

看下邊界檢查的核心IR代碼:

***** BB02
STMT00002 ( 0x004[E-] ... 0x009 )
   [000013] ---XG+-----                         *  JTRUE     void  
   [000012] N--XG+-N-U-                         \--*  EQ        int   
   [000034] ---XG+-----                            +--*  COMMA     int   
   [000026] ---X-+-----                            |  +--*  BOUNDS_CHECK_Rng void  
   [000008] -----+-----                            |  |  +--*  LCL_VAR   int    V01 loc0         
   [000025] ---X-+-----                            |  |  \--*  ARR_LENGTH int   
   [000007] -----+-----                            |  |     \--*  LCL_VAR   ref    V00 arg0         
   [000035] n---G+-----                            |  \--*  IND       int   
   [000033] -----+-----                            |     \--*  ARR_ADDR  byref int[]
   [000032] -----+-----                            |        \--*  ADD       byref 
   [000023] -----+-----                            |           +--*  LCL_VAR   ref    V00 arg0         
   [000031] -----+-----                            |           \--*  ADD       long  
   [000029] -----+-----                            |              +--*  LSH       long  
   [000027] -----+---U-                            |              |  +--*  CAST      long <- uint
   [000024] -----+-----                            |              |  |  \--*  LCL_VAR   int    V01 loc0         
   [000028] -----+-N---                            |              |  \--*  CNS_INT   long   2
   [000030] -----+-----                            |              \--*  CNS_INT   long   16
   [000011] -----+-----                            \--*  CNS_INT   int    42

------------ BB03 [00D..019) -> BB02 (cond), preds={BB02} succs={BB04,BB02}

這種看着牛逼轟轟的代碼,正是IR。從最裏面看起,意思在註釋裏。

[000031] -----+-----                            |           \--*  ADD       long //把LSH計算的結果加上16,這個16就是下面的CNS_INT long 16的16.
     [000029] -----+-----                            |              +--*  LSH       long  //LSH表示把數組索引左移2位。這個2就是下面的CNS_INT long 2裏面的2
     [000027] -----+---U-                            |              |  +--*  CAST      long <- uint//把數組索引的類型從uint轉換轉換成long類型
     [000024] -----+-----                            |              |  |  \--*  LCL_VAR   int    V01 loc0 //讀取本地變量V01,實際上就是數組arrar的索引。
     [000028] -----+-N---                            |              |  \--*  CNS_INT   long   2 //這個2是左移的位數
     [000030] -----+-----                            |              \--*  CNS_INT   long   16//被ADD相加的數值16

繼續看

 |  \--*  IND       int   
   [000033] -----+-----                            |     \--*  ARR_ADDR  byref int[]
   [000032] -----+-----                            |        \--*  ADD       byref //把前面計算的結果與array數組的地址相加。實際上就是 array + i*4+-x10。一個索引佔4個字節,methodtable和array.length各佔8字節,這個表達式的結果就是索引位i的array的值,也就是array[i]這個數值。
   [000023] -----+-----                            |           +--*  LCL_VAR   ref    V00 arg0 //獲取本地變量V00的地址,這個地址實際上就是數組array的地址。
   [000031] -----+-----                            |           \--*  ADD       long  
   [000029] -----+-----                            |              +--*  LSH       long  
   [000027] -----+---U-                            |              |  +--*  CAST      long <- uint
   [000024] -----+-----                            |              |  |  \--*  LCL_VAR   int    V01 loc0         
   [000028] -----+-N---                            |              |  \--*  CNS_INT   long   2
   [000030] -----+-----                            |              \--*  CNS_INT   long   16

繼續看

  [000013] ---XG+-----                         *  JTRUE     void //是或者否都進行相應的跳轉
   [000012] N--XG+-N-U-                         \--*  EQ        int //判斷獲取的array[i]是否等於42,這個42是CNS_INT int 42裏的42
   [000034] ---XG+-----                            +--*  COMMA     int //計算它的兩個值,獲取第二個值也就是array[i]
   [000026] ---X-+-----                            |  +--*  BOUNDS_CHECK_Rng void  
   [000008] -----+-----                            |  |  +--*  LCL_VAR   int    V01 loc0 //數組的索引i值
   [000025] ---X-+-----                            |  |  \--*  ARR_LENGTH int //獲取數組長度
   [000007] -----+-----                            |  |     \--*  LCL_VAR   ref    V00 arg0  //數組的長度
   [000035] n---G+-----                            |  \--*  IND       int   //獲取array[i]的值
   [000033] -----+-----                            |     \--*  ARR_ADDR  byref int[] //獲取剛剛array數組地址
    //中間省略,上面已經寫過了。
 [000011] -----+-----                            \--*  CNS_INT   int    42

那麼翻譯成C# Code如下:

if(array[i]==42)
{
  return true;
}
return false

這裏還沒有循環,因爲循環在其它的Basic Block塊,這裏是BB02塊。那麼下面就是對着BB02進行優化變形,最終形成了如上邊界檢查去除所示的結果。關於這點,下篇再看。

結尾

作者:江湖評談
原文:在此處
文章首發在公衆號上,歡迎關注。
image

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