1. ホーム
  2. c++

[解決済み] 実際には、なぜコンパイラによってint x = ++i ++i; の計算値が異なるのでしょうか?

2022-05-02 09:22:52

質問

このコードを考えてみましょう。

int i = 1;
int x = ++i + ++i;

このコードがコンパイルされると仮定して、コンパイラが何をするかについて、いくつかの推測があります。

  1. 両方 ++i 戻る 2 となり、その結果 x=4 .
  2. ++i リターン 2 を返し、もう一方は 3 となり、結果として x=5 .
  3. 両方 ++i 戻る 3 となり、その結果 x=6 .

私には、2番目の可能性が最も高いと思われます。2つのうち1つは ++ 演算子が実行され i = 1 を使用する場合、その i がインクリメントされ、その結果 2 が返されます。次に、2番目の ++ 演算子が実行され i = 2 の場合、その i がインクリメントされ、その結果 3 が返されます。次に 23 を足すと 5 .

しかし、このコードをVisual Studioで実行したところ、結果は 6 . 私はコンパイラについてもっと理解しようとしているのですが、何がどうなって 6 . 私の唯一の推測は、コードが何らかの "built-in" 並行処理で実行される可能性があるということです。2つの ++ 演算子が呼び出され、それぞれインクリメントされた i が返され、その後、両者とも 3 . これは、私のコールスタックの理解に反するので、説明する必要があります。

はどのような(合理的な)ことができるのでしょうか? C++ という結果になるようなコンパイラを使用します。 4 または結果や 6 ?

備考

この例は、Bjarne Stroustrup著「Programming」に未定義の動作の例として掲載されています。Principles and Practice using C++ (C++ 14)に未定義動作の例として掲載されています。

参照 シナモンさんのコメント .

解決方法は?

コンパイラは、あなたのコードを非常に単純な命令に分割し、最適と思われる方法でそれらを再結合し、配置します。

コード

int i = 1;
int x = ++i + ++i;

は、以下の命令で構成されています。

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->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. 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. 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++が出している結果を得ることができます。

どちらの最適化パスでも、何もしないために命令が削除されない限り、重要な順序依存性は維持されていることに注意してください。