Should I save the length of the array to a local variable in C #

    Very often I notice that people write like this:

    var length = array.Length;
    for (int i = 0; i < length; i++) {
        //do smth
    }

    They write this in the hope of speeding up the loop, thinking that creating a local variable eliminates the need for the CLR to call the getter for Array.Length each time. I decided once and for all for myself to understand it’s worth doing this or you can save your time and write without a temporary variable.

    First, consider two methods in C #:

    publicintWithoutVariable() {
        int sum = 0;
        for (int i = 0; i < array.Length; i++) {
            sum += array[i];
        }
        return sum;
    }
    publicintWithVariable() {
        int sum = 0;
        int length = array.Length;
        for (int i = 0; i < length; i++) {
            sum += array[i];
        }
        return sum;
    }

    And let's look at the code that is obtained after JIT compilation in the release on .NET Framework 4.7.2 under LegacyJIT-x86:
    WithoutVariable ()
    ; int sum = 0;
        xor   edi ,  edi
    ; int i = 0;
        xor   esi ,  esi
    ; int [] localRefToArray = this.array;
        mov   edx ,  dword  ptr  [ ecx + 4 ]
    ; int arrayLength = localRefToArray.Length;
        mov   ecx ,  dword  ptr  [ edx + 4 ]
    ; if (arrayLength == 0) return sum;
        test  ecx ,  ecx
        jle   exit
    ; int arrayLength2 = localRefToArray.Length;
        mov   eax,  dword  ptr  [ edx + 4 ]  
    ; if (i> = arrayLength2)
    ; throw new IndexOutOfRangeException ();
      loop :
        cmp   esi ,  eax
        jae   056e2d31
    ; sum + = localRefToArray [i];
        add   edi ,  dword  ptr  [ edx + esi * 4 + 8 ]
    ; i ++;
        inc   esi
    ; if (i <arrayLength) goto loop
        cmp   ecx ,  esi
        jg   loop
    ; return sum;
      exit :
        mov  eax ,  edi
    WithVariable ()
    ; int sum = 0;
        xor   esi ,  esi
    ; int [] localRefToArray = this.array;
        mov   edx ,  dword  ptr  [ ecx + 4 ]
    ; int arrayLength = localRefToArray.Length;
        mov   edi ,  dword  ptr  [ edx + 4 ]
    ; int i = 0;
        xor   eax ,  eax
    ; if (arrayLength == 0) return sum;
        test  edi ,  edi
        jle   exit
    ; int arrayLength2 = localRefToArray.Length;
        mov   ecx , dword  ptr  [ edx + 4 ]
    ; if (i> = arrayLength2)
    ; throw new IndexOutOfRangeException ();
      loop :
        cmp   eax ,  ecx
        jae   05902d31
    ; sum + = localRefToArray [i];
        add   esi ,  dword  ptr  [ edx + eax * 4 + 8 ]
    ; i ++;
        inc   eax
    ; if (i <arrayLength) goto loop
        cmp   eax ,  edi
        jl    loop
    ; return sum;
      exit :
        mov   eax,  esi

    Screenshot comparison in Meld


    It is easy to see that the number of assembler instructions is exactly the same - 15 pieces. And the logic of these instructions also almost completely coincides. There is only a slight difference in the order of initialization of the variables and the comparison of whether to continue the cycle. It can be noted that in both cases the length of the array is entered into registers two times before the cycle:

    • to check for 0 ( arrayLength );
    • and a temporary variable to check in the loop condition ( arrayLength2 );

    It turns out that both of the above methods are compiled into the same code, but the first one is written faster and there is no clear gain in execution speed.

    The above assembler code led me to some thoughts and I decided to check out a couple more methods:

    publicintWithoutVariable() {
        int sum = 0;
        for(int i = 0; i < array.Length; i++) {
            sum += array[i] + array.Length;
        }
        return sum;
    }
    publicintWithVariable() {
        int sum = 0;
        int length = array.Length;
        for(int i = 0; i < length; i++) {
            sum += array[i] + length;
        }
        return sum;
    }

    Now the current element and the length of the array are summed, only in the first case the length of the array is requested each time, and in the second it is stored once in a local variable. Let's look at the assembler code of these methods:
    WithoutVariable ()
    int  sum =  0 ;
        xor   edi ,  edi
    int  i =  0 ; 
        xor   esi ,  esi
    int [ ]  localRefToArray = this . array ; 
        mov   edx ,  dword  ptr  [ ecx + 4 ]
    int  arrayLength = localRefToArray . Length ; 
        mov   ebx ,  dword  ptr  [ edx + 4 ]
    if  (arrayLength ==  0 )  return sum ;
        test  ebx ,  ebx
        jle   exit
    int  arrayLength2 = localRefToArray . Length ;
        mov   ecx ,  dword  ptr  [ edx + 4 ]
    if  ( i> = arrayLength2 )
      throw new IndexOutOfRangeException ( ) ; 
      loop :
        cmp   esi ,  ecx  
        jae   05562d39
    int  t = array [ i ] ; 
        mov   eax ,  dword  ptr  [ edx + esi * 4 + 8 ]
    t  + = sum ;
        add   eax ,  edi
    t + = arrayLength ;
        add   eax ,  ebx
    sum = t ; 
        mov   edi ,  eax
    i ++ ;
        inc   esi
    if  ( i <arrayLength )  goto  loop
        cmp   ebx ,  esi
        jg    loop
    return sum ;
      exit :
        mov   eax ,  edi
    WithVariable ()
    int  sum =  0 ;
        xor   esi ,  esi
    int [ ]  localRefToArray = this . array ;  
        mov   edx ,  dword  ptr  [ ecx + 4 ]
    int  arrayLength = localRefToArray . Length ;  
        mov   ebx ,  dword  ptr  [ edx + 4 ]
    int  i =  0 ;  
        xor   ecx ,  ecx
    if  (arrayLength ==  0 )  ( return sum ;) 
        test   ebx ,  ebx
        jle   exit
    int  arrayLength2 = localRefToArray . Length ;
        mov    edi ,  dword  ptr  [ edx + 4 ]
    if  ( i> = arrayLength2 )
     throw new IndexOutOfRangeException ( ) ;
     loop :
        cmp   ecx ,  edi
        jae   04b12d39
    int  t = array [ i ]; 
        mov   eax ,  dword  ptr  [ edx + ecx * 4 + 8 ]
    t  + = sum ; 
        add   eax ,  esi
    t + = arrayLength ; 
        add   eax ,  ebx
    sum = t ; 
        mov   esi ,  eax
    i ++ ; 
        inc   ecx
    if  ( i <arrayLength )  goto  loop  
        cmp   ecx ,  ebx
        jl   loop
    return sum ;
      exit :
        mov   eax ,  esi

    Screenshot comparison in Meld


    The number of instructions in these two methods and the instructions themselves are almost exactly the same, again there is a difference only in the order of initializing the variables and checking for a continuation of the cycle. Moreover, it can be noted that only the array is taken into account in calculating the sum.Length, which was taken the very first. Obviously, this is:
    int  arrayLength2 = localRefToArray . Length ;
        mov      edi ,  dword  ptr  [ edx + 4 ]
    if  ( i> = arrayLength2 )  throw new IndexOutOfRangeException ( ) ;
        cmp      ecx ,  edi
        jae      04b12d39
    in all four methods, there is an inline check for going beyond the array and it is performed for each element of the array.

    It is possible to draw the first conclusion that it is a waste of time to start an additional variable in order to try to speed up the execution of a cycle. the compiler will do everything for us. Setting a variable with a cycle length makes sense only to increase the readability of the code.

    A completely different situation with ForEach. Take three methods:

    publicintForEachWithoutLength() {
        int sum = 0;
        foreach (int i in array) {
            sum += i;
        }
        return sum;
    }
    publicintForEachWithLengthWithoutLocalVariable() {
        int sum = 0;
        foreach (int i in array) {
            sum += i + array.Length;
        }
        return sum;
    }
    publicintForEachWithLengthWithLocalVariable() {
        int sum = 0;
        int length = array.Length;
        foreach (int i in array) {
            sum += i + length;
        }
        return sum;
    }

    And look at their code after JIT:

    ForEachWithoutLength ()
    ;int sum = 0;
        xor  esi, esi
    ;int[] localRefToArray = this.array; 
        mov  ecx, dword ptr [ecx+4]
    ;int i = 0;
        xor  edx, edx
    ;int arrayLength = localRefToArray.Length; 
        mov  edi, dword ptr [ecx+4]
    ;if (arrayLength == 0) goto exit;
        test  edi, edi
        jle  exit
    ;int t = array[i];
      loop:
        mov  eax, dword ptr [ecx+edx*4+8]
    ;sum+=i;
        add  esi, eax
    ;i++;
        inc  edx
    ;if (i < arrayLength) goto loop
        cmp  edi, edx
        jg  loop
    ;return sum;
      exit:
        mov  eax, esi

    ForEachWithLengthWithoutLocalVariable ()
    ;int sum = 0;
        xor  esi, esi
    ;int[] localRefToArray = this.array;  
        mov  ecx, dword ptr [ecx+4]
    ;int i = 0; 
        xor  edx, edx
    ;int arrayLength = localRefToArray.Length;  
        mov  edi, dword ptr [ecx+4]
    ;if (arrayLength == 0) goto exit
        test  edi, edi
        jle  exit
    ;int t = array[i];  
      loop:
        mov  eax, dword ptr [ecx+edx*4+8]
    ;sum+=i; 
        add  esi, eax
    ;sum+=localRefToArray.Length;
        add  esi, dword ptr [ecx+4]
    ;i++;
        inc  edx
    ;if (i < arrayLength) goto loop 
        cmp  edi, edx
        jg  loop
    ;return sum;
      exit:
        mov  eax, esi

    ForEachWithLengthWithLocalVariable ()
    ;int sum = 0;
        xor  esi, esi
    ;int[] localRefToArray = this.array;   
        mov  edx, dword ptr [ecx+4]
    ;int length = localRefToArray.Length;   
        mov  ebx, dword ptr [edx+4]
    ;int i = 0;  
        xor  ecx, ecx
    ;int arrayLength = localRefToArray.Length;   
        mov  edi, dword ptr [edx+4]
    ;if (arrayLength == 0) goto exit; 
        test  edi, edi
        jle  exit
    ;int t = array[i];  
      loop: 
        mov  eax, dword ptr [edx+ecx*4+8]
    ;sum+=i;  
        add  esi, eax
    ;sum+=length ; 
        add  esi, ebx
    ;i++; 
        inc  ecx
    ;if (i < arrayLength) goto loop
        cmp  edi, ecx
        jg  loop
    ;return sum;
      exit:
        mov  eax, esi

    The first thing that catches your eye is that there are fewer assembler instructions compared to the for loop (for example, 12 instructions came out in the foreach for simple summation of elements, in for 15).

    Screenshot comparison of For and ForEach


    In fact, if you write a benchmark for vs foreach on 1,000,000 elements of an array, you get this picture for

    sum+=array[i];
    MethodItemscountMeanErrorStddevMedianRatioRatioSD
    ForEach1,000,0001.401 ms0.2691 ms0.7935 ms1.694 ms1.000.00
    For1,000,0001.586 ms0.3204 ms0.9447 ms1.740 ms1.230.65
    and for

    sum+=array[i] + array.Length;
    MethodItemscountMeanErrorStddevMedianRatioRatioSD
    ForEach1,000,0001.703 ms0.3010 ms0.8874 ms1.726 ms1.000.00
    For1,000,0001.715 ms0.2859 ms0.8430 ms1.956 ms1.130.56
    ForEach for arrays works faster than For. Why it happens? To understand this, you need to compare the code after JIT.

    Screenshot comparison of all three options foreach


    Let's look at ForEachWithoutLength. In it, the length of the array is requested only once and there are no checks on the output of elements outside the array. This happens because the ForEach cycle firstly prohibits changing the collection inside the cycle, and secondly, it will definitely not go beyond the collection. Because of this, the JIT can afford to generally remove the check for output from the array.

    Now let's take a close look at the ForEachWithLengthWithoutLocalVariable method. In his code, there is only one strange moment in that sum + = length occurs not with the local variable arrayLength stored previously, but with the new one that the program asks every time from memory. It turns out that the memory accesses for the array length will be N + 1, where N is the length of the array.

    It remains to consider the method ForEachWithLengthWithLocalVariable. Its code is no different from ForEachWithLengthWithoutLocalVariable, except for working with a long array. The compiler again generated the local variable arrayLength, with which it checks that the array is not empty, but the compiler honestly preserved our explicit local variable length and addition in the body of the loop already happens to it. It turns out that this method only accesses memory twice to determine the length of the array. This difference in the number of memory accesses in real applications is very difficult to notice.

    The assembler code of the methods examined was so simple because the methods themselves are simple. If there were more parameters in the method, there would already be work with the stack, the variables would not only be stored in registers and perhaps there would be more checks, but the basic logic would remain the same: introducing a local variable with an array length can make sense only to increase the code readability . In addition, it turned out that Foreach in an array is often faster than For.

    Also popular now: