Consider this code:
int i = 1;
int x = ++i + ++i;
We have some guesses for what a compiler might do for this code, assuming it compiles.
- both
++i
return2
, resulting inx=4
. - one
++i
returns2
and the other returns3
, resulting inx=5
. - both
++i
return3
, resulting inx=6
.
To me, the second seems most likely. One of the two ++
operators is executed with i = 1
, the i
is incremented, and the result 2
is returned. Then the second ++
operator is executed with i = 2
, the i
is incremented, and the result 3
is returned. Then 2
and 3
are added together to give 5
.
However, I ran this code in Visual Studio, and the result was 6
. I'm trying to understand compilers better, and I'm wondering what could possibly lead to a result of 6
. My only guess is that the code could be executed with some "built-in" concurrency. The two ++
operators were called, each incremented i
before the other returned, and then they both returned 3
. This would contradict my understanding of the call stack, and would need to be explained away.
What (reasonable) things could a C++
compiler do that would lead to a result of 4
or a result or 6
?
Note
This example appeared as an example of undefined behavior in Bjarne Stroustrup's Programming: Principles and Practice using C++ (C++ 14).
See cinnamon's comment.
Best Answer
The compiler takes your code, splits it into very simple instructions, and then recombines and arranges them in a way that it thinks optimal.
The code
consists of the following instructions:
But despite this being a numbered list the way I wrote it, there are only a few ordering dependencies here: 1->2->3->4->5->10->11 and 1->6->7->8->9->10->11 must stay in their relative order. Other than that the compiler can freely reorder, and perhaps eliminate redundancy.
For example, you could order the list like this:
Why can the compiler do this? Because there's no sequencing to the side effects of the increment. But now the compiler can simplify: for example, there's a dead store in 4: the value is immediately overwritten. Also, tmp2 and tmp4 are really the same thing.
And now everything to do with tmp1 is dead code: it's never used. And the re-read of i can be eliminated too:
Look, this code is much shorter. The optimizer is happy. The programmer is not, because i was only incremented once. Oops.
Let's look at something else the compiler can do instead: let's go back to the original version.
The compiler could reorder it like this:
and then notice again that i is read twice, so eliminate one of them:
That's nice, but it can go further: it can reuse tmp1:
Then it can eliminate the re-read of i in 6:
Now 4 is a dead store:
and now 3 and 7 can be merged into one instruction:
Eliminate the last temporary:
And now you get the result that Visual C++ is giving you.
Note that in both optimization paths, the important order dependencies were preserved, insofar as the instructions weren't removed for doing nothing.