1. ホーム
  2. c++

[解決済み] C++0xで "while(1); "を排除する最適化

2022-04-26 14:18:47

質問

更新しました!下記をご覧ください。

C++0xでは、次のスニペットに対してコンパイラが"Hello"を表示できると聞いたり読んだりしたことがあります。

#include <iostream>

int main() {
  while(1) 
    ;
  std::cout << "Hello" << std::endl;
}

どうやらスレッドと最適化能力に関係があるようです。これは多くの人を驚かせることができそうですが。

なぜこれを許可する必要があったのか、どなたか良い説明をお持ちの方はいらっしゃいますか?参考までに、最新の C++0x ドラフトでは、以下のように書かれています。 6.5/5

for文の場合、for-init文の外側にあるループのこと。

  • ライブラリI/O関数を一切呼び出さない。
  • volatileオブジェクトにアクセスしたり変更したりしない。
  • 同期操作(1.10)やアトミック操作(第29項)を行わない。

は、実装によって終了することが想定される。[注:これはコンパイラによる変換を可能にするためのものです。 終了が証明できない場合でも、空のループの削除などのメーションが可能です。- エンドノート ]。

編集する

この洞察に満ちた記事 その基準文について

<ブロッククオート

残念ながら、undefined behaviorという言葉は使われていません。しかし、「コンパイラはPと仮定してよい」と書かれている場合、not-Pのプロパティを持つプログラムは未定義のセマンティクスを持っているということを暗に示しているのです。

上記のプログラムに対して、コンパイラは "Bye" と出力してよいのでしょうか?


さらに洞察に満ちた このスレッド これはC言語の類似の変更に関するもので、上記のリンク先の記事にあるGuyが始めたものです。他の有用な事実の中で、彼らはC++0xにも適用されると思われる解決策を提示しています( 更新 : n3225ではもう使えません - 下記参照!)

endless:
  goto endless;

コンパイラは、ループではなく、ジャンプなので、それを最適化することは許されないようです。別の人が、C++0xとC201Xの変更案をまとめています。

<ブロッククオート

ループを書くことで、プログラマは次のことを主張している。 どちらか その ループは目に見える動作(I/Oの実行、アクセス)をします。 揮発性オブジェクト、同期またはアトミック操作の実行)。 または 最終的に終了すること。 その前提に反すると 副作用のない無限ループを書くことで、私は嘘をついていることになります。 コンパイラは、私のプログラムの動作が未定義であることを示します。 (もし私が幸運なら。 コンパイラが警告してくれるかもしれません)。 言語が提供しない (無限ループを表現する方法がない? 目に見える動作


2011年3月1日、n3225で更新。委員会は、1.10/24にテキストを移動し、次のように述べています。

<ブロッククオート

実装は、どのスレッドも最終的に以下のいずれかを行うことを想定してよい。

  • を終了させる。
  • ライブラリI/O関数を呼び出す。
  • volatileオブジェクトへのアクセスまたは修正、または
  • 同期操作またはアトミック操作の実行。

goto トリックは ではなく はもう使えない!

解決方法は?

<ブロッククオート

なぜこれを許可する必要があったのか、どなたか良い説明をお持ちの方はいらっしゃいませんか?

はい、Hans Boehmはその根拠を次のように示しています。 N1528: なぜ無限ループでは未定義の動作になるのですか? これはWG14の文書ですが、その根拠はC++にも適用され、この文書はWG14とWG21の両方に言及されています。

<ブロッククオート

N1509が正しく指摘するように、現在の草案は本質的に 6.8.5p6では無限ループが未定義な動作になっています。大きな問題点として そのため、潜在的なコードの移動が可能になります。 非終了ループ 例えば、以下のようなループがあるとします。 ここで、countとcount2はグローバル変数(またはそのアドレスを持つ)である。 pはローカル変数で、そのアドレスはまだ取得されていない。

for (p = q; p != 0; p = p -> next) {
    ++count;
}
for (p = q; p != 0; p = p -> next) {
    ++count2;
}

この2つのループを統合して、次のようなループに置き換えることは可能でしょうか?

for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}

6.8.5p6 の無限ループの特別な免除がなければ、これは は許されないでしょう。最初のループが終了しないのは、q が循環リストを指している場合、オリジナルは決して count2 に書き込まれません。したがって をアクセスする別のスレッドと並列に実行することができます。 を更新する。これは変換されたバージョンではもはや安全ではありません。 無限ループにもかかわらず count2 にアクセスする。このため 変換はデータレースを引き起こす可能性があります。

このような場合、コンパイラがこのような問題を解決できる可能性は極めて低いです。 ループの終了を証明するためには、qがループを指していることを理解しなければなりません。 を非周期リストに変換することは、ほとんどの場合、その能力を超えていると思います。 主流のコンパイラは、プログラム全体がないと不可能なことが多い。 の情報です。

非終了ループによる制限は、そのループが終了する前に、そのループが終了してしまうという制限です。 コンパイラが最適化できない終端ループの最適化。 の最適化だけでなく、終了を証明することもできます。 非終了ループ 前者の方がはるかに一般的です。 また、後者の方が最適化する上で興味深い場合が多い。

また、ループ変数が整数であるforループも明らかに存在します。 コンパイラが終端を証明することが困難で、かつ ループを再構築することは困難です。 6.8.5p6を使用しない場合。のようなものでも

for (i = 1; i != 15; i += 2)

または

for (i = 1; i <= 10; i += j)

を処理することは自明ではないようです。(前者の場合、基本的な数 終了を証明するためには理論が必要であり、後者の場合は の値について何か知っている必要があります。ラップアラウンド は、符号なし整数の場合、この推論をさらに複雑にする可能性があります)。

この問題は、ほとんどすべてのループ再構築に適用されるようです。 コンパイラの並列化や キャッシュ最適化変換は、今後ますます増加すると思われます。 の重要性が増しており、数値計算コードでは既に重要な場合が多い。 これは、次のような利点に対して、かなりのコストになりそうです。 無限ループを最も自然な形で書けるようになる。 特に、ほとんどの人は意図的に無限ループを書くことはないので。

C言語と大きく違うのは C11 では、定数式である式を制御するための例外が用意されている これはC++とは異なり、あなたの具体的な例をC11でうまく定義しています。