.Net8 Top Technology: IR Analysis of Boundary Check (Caution)

Foreword

The reason why the language C# is known as a safe, object-oriented language. The word “safety” is not a joke. Because the JIT checks any value that might be outside the allocated range in order to keep it within safe boundaries. There are two concepts here, one is boundary checking, and the other is IR parsing. The generation of the latter is a guarantee of the functionality of the former. What is IR? You think IL is an intermediate language, but it is not. There is also an intermediate representation of IR. One of the top technologies of .Net8, few people know about it. This article takes a look at these two technologies.

Summary

1. Defects of boundary checking
The bounds check here takes the bounds check of the array as an example, look at the C# code
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 does not know whether the i index in the array array[i] exceeds the length of the array array. So every cycle will check the size of the index, if it exceeds, an exception will be reported, and the cycle will continue if it does not exceed. This function is called boundary checking. It is automatically added by the .Net6 JIT, but it has flaws.

The disadvantage is that checking every cycle greatly consumes the running efficiency of the code. In order to avoid this defect, is it possible to determine whether the length of the array array is less than or the maximum value of the loop before the loop. Through this one-time judgment, it replaces the judgment of each cycle to maximize the efficiency of code operation.
This is possible in .Net7.
.Net7 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

As mentioned above, the judgment of the boundary check is placed outside the for loop. if and else are split into fast and slow paths, with the former being optimized. The reverse into C# code is as follows

if(array!=null & amp; & amp; array.length >=0x12345)//The array cannot be empty, and the length of the array cannot be less than the length of the loop. Otherwise bound overflow
{
   for(int i=0;i<0x12345;i++)
   {
     if(array[i]==42)//The boundary is no longer checked here
     {
       return true;
     }
   }
   return false;
}
else
{
  for(int i=0;i<0x2345;i++)
  {
   if(i<array. length)
   {
     if(array[i]==42)
     return true;
   }
  }
  return flase;
}

Boundary check is not the focus of this section, the focus is how this boundary check is generated through IR. Because there is no IL code.

2. Generation of IR
It is conventionally believed that the running process of C# is:
C# Code->
IL ->
Machine Code
It is generally believed that IL is an intermediate language, or bytecode. But there is actually another layer inside the JIT. as follows:
C# Code ->
IL ->
IR ->
Machine Code
This IR is to perform various operations on IL. The most important point is various optimizations and deformations. Here’s a look at how IR performs boundary check optimization on IL.

Look at the core IR code for bounds checking:

****** 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}

This kind of code that looks awesome and doesn’t know what to write is exactly IR. From the inside, the meaning is in the comments.

[000031] ----- + ----- | \--* ADD long //Add 16 to the result of LSH calculation, this 16 is 16 of the following CNS_INT long 16.
     [000029] ----- + ----- | + --* LSH long //LSH means to shift the array index to the left by 2 bits. This 2 is the 2 in the CNS_INT long 2 below
     [000027] ----- + ---U- | | + --* CAST long <- uint//Convert the type of array index from uint to long type
     [000024] ----- + ----- | | | \--* LCL_VAR int V01 loc0 //Read the local variable V01, which is actually the index of the array arrar.
     [000028] ----- + -N--- | | \--* CNS_INT long 2 //This 2 is the digit of left shift
     [000030] ----- + ----- | \--* CNS_INT long 16//The value 16 added by ADD

keep watching

| \--* IND int
   [000033] ----- + ----- | \--* ARR_ADDR byref int[]
   [000032] ----- + ----- | \--* ADD byref //Add the result of the previous calculation to the address of the array array. It is actually array + i*4 + -x10. An index occupies 4 bytes, and methodtable and array.length each occupy 8 bytes. The result of this expression is the value of the array at index i, that is, the value of array[i].
   [000023] ----- + ----- | + --* LCL_VAR ref V00 arg0 //Get the address of the local variable V00, this address is actually the address of the array 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

keep watching

[000013] ---XG + ----- * JTRUE void //jump accordingly if yes or no
   [000012] N--XG + -N-U- \--* EQ int //Judge whether the acquired array[i] is equal to 42, this 42 is 42 in CNS_INT int 42
   [000034] ---XG + ----- + --* COMMA int //Calculate its two values, get the second value which is array[i]
   [000026] ---X- + ----- | + --* BOUNDS_CHECK_Rng void
   [000008] ----- + ----- | | + --* LCL_VAR int V01 loc0 //The index i value of the array
   [000025] ---X- + ----- | | \--* ARR_LENGTH int //Get array length
   [000007] ----- + ----- | | \--* LCL_VAR ref V00 arg0 //The length of the array
   [000035] n---G + ----- | \--* IND int //Get the value of array[i]
   [000033] ----- + ----- | \--* ARR_ADDR byref int[] //Get the address of the array just now
    //Omitted in the middle, it has been written above.
 [000011] ----- + ----- \--* CNS_INT int 42

Then the translation into C# Code is as follows:

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

There is no loop here, because the loop is in another Basic Block, here is the BB02 block. Then the following is to optimize the deformation of BB02, and finally form the result shown in the above boundary check removal. On this point, see the next article.

End

Author: Jianghu Review