.Net8顶级技术:边界检查之IR解析(慎入)

Posted 六朝一洗繁华尽 四始重删雅颂分

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了.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进行优化变形,最终形成了如上边界检查去除所示的结果。关于这点,下篇再看。

结尾

作者:江湖评谈
原文:在此处

慎入秘境探索之一个.NET 对象从内存分配到内存回收