Sunday, September 15, 2019

The C++98 and C++11 for-each range differences

While investigating a performance hit between two code paths, I discovered some interesting differences between the C++98 [1] and C++11 [2] for-each range implementations.

Here are the two different for-each ranges to compare:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int main()
{
    constexpr unsigned numElts = 7;
    unsigned values[numElts]{2, 4, 6, 8, 12, 21, 101};

    for(unsigned idx = 0; idx < numElts; ++idx)
    {
      ++values[idx];
    }

    for(auto & v : values)
    {
      ++v;
    }
}

The first for-each range is a classical loop to access one-by-one all the elements of a list and perform a basic modification to each element. The second for-each range mimics the exact same processing, but it's written using the new C++11 for-each range writing.

Now use C++ Insights [3] to see the differences between the two implementations:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
  constexpr const unsigned int numElts = 7;
  unsigned int values[7] = {2, 4, 6, 8, 12, 21, 101};

  for(unsigned int idx = 0; idx < numElts; ++idx) 
  {
    ++values[idx];
  }

  {
    unsigned int (&__range1)[7] = values;

    for(unsigned int * __begin1 = __range1, *__end1 = __range1 + 7l; 
        __begin1 != __end1; ++__begin1) 
    {
      unsigned int & v = *__begin1;
      ++v;
    }
  }
}

Note that the C++98 does not change (i.e. only a minor change to clearly define the idx variable type).

Below is the assembly of the C++98 for-loop (i.e. use the online compiler godbolt [4]):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
        mov     DWORD PTR [rbp-4], 0
.L3:
        cmp     DWORD PTR [rbp-4], 6
        ja      .L2
        mov     eax, DWORD PTR [rbp-4]
        mov     eax, DWORD PTR [rbp-80+rax*4]
        lea     edx, [rax+1]
        mov     eax, DWORD PTR [rbp-4]
        mov     DWORD PTR [rbp-80+rax*4], edx
        add     DWORD PTR [rbp-4], 1
        jmp     .L3
.L2:

Note how the gcc converts the C++11 for-each range to a classical C++98 for loop using pointer arithmetic.

Below is the assembly of the C++11 for-each range (i.e. use the online compiler godbolt [4]):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
        lea     rax, [rbp-80]
        mov     QWORD PTR [rbp-32], rax
        mov     rax, QWORD PTR [rbp-32]
        mov     QWORD PTR [rbp-16], rax
        mov     rax, QWORD PTR [rbp-32]
        add     rax, 28
        mov     QWORD PTR [rbp-40], rax
.L5:
        mov     rax, QWORD PTR [rbp-16]
        cmp     rax, QWORD PTR [rbp-40]
        je      .L4
        mov     rax, QWORD PTR [rbp-16]
        mov     QWORD PTR [rbp-48], rax
        mov     rax, QWORD PTR [rbp-48]
        mov     eax, DWORD PTR [rax]
        lea     edx, [rax+1]
        mov     rax, QWORD PTR [rbp-48]
        mov     DWORD PTR [rax], edx
        add     QWORD PTR [rbp-16], 4
        jmp     .L5
.L4:



The C++11 implementation needs 9 assembly instructions for the C++98 implementation. But it requires 12 assembly instructions for the C++11 implementation. If you now consider all the loop (i.e. the loop content plus all declarations) then the difference is now 10 vs. 19.


References:

[1] C++98 for loop https://en.cppreference.com/w/cpp/language/for
[2] C++11 for-each range https://en.cppreference.com/w/cpp/language/range-for
[3] Andreas Fertig is the creator of C++ Insight, https://cppinsights.io/
[4] The online compiler from Matt Godbolt, https://godbolt.org/


[keywords: for loop, for-each range, c++98, c++11]

No comments:

Post a Comment