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]