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.
ベストアンサー1
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
int i = 1;
int x = ++i + ++i;
consists of the following instructions:
1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x
But despite this being a numbered list the way I wrote it, there are only a few ordering dependenciesここで、1->2->3->4->5->10->11 と 1->6->7->8->9->10->11 は相対的な順序を維持する必要があります。それ以外は、コンパイラは自由に順序を変更でき、冗長性を排除できる可能性があります。
たとえば、リストを次のように順序付けることができます。
1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x
なぜコンパイラはこれを実行できるのでしょうか? それは、増分による副作用に順序がないためです。しかし、コンパイラは単純化できます。たとえば、4 には無効なストアがあり、値はすぐに上書きされます。また、tmp2 と tmp4 は実際には同じものです。
1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x
これで、tmp1 に関連するものはすべてデッド コードになりました。つまり、使用されません。また、i の再読み込みも削除できます。
1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x
ご覧のとおり、このコードはずっと短くなりました。オプティマイザーは満足です。プログラマーは満足していません。なぜなら、i は 1 回しか増分されていないからです。おっと。
代わりにコンパイラが実行できる別の方法を見てみましょう。元のバージョンに戻りましょう。
1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x
コンパイラは次のように順序を変更できます。
1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x
そして、i が 2 回読み取られていることに再度注意し、そのうちの 1 つを削除します。
1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x
それは素晴らしいですが、さらに進んで、tmp1 を再利用できます。
1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x
すると、6 の i の再読み込みが不要になります。
1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x
現在、4 はデッド ストアです。
1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x
そして、3 と 7 を 1 つの命令に結合できるようになりました。
1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x
最後の一時的なものを削除します。
1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x
そして、Visual C++ が提供する結果が得られます。
どちらの最適化パスでも、何もしない命令が削除されない限り、重要な順序の依存関係が保持されていることに注意してください。