C11は、この質問に対する答えを明確にしています。ドラフトC11標準セクションで、6.8.5
反復ステートメントに次の段落が追加されています。
制御式が定数式ではない反復ステートメント156)は、入出力操作を実行せず、揮発性オブジェクトにアクセスせず、本体で同期またはアトミック操作を実行せず、式を制御します。ステートメント)その式-3は、実装によって終了すると見なされる場合があります。157)
脚注157
は次のように述べています。
これは、終了が証明できない場合でも、空のループの削除などのコンパイラ変換を可能にすることを目的としています。
だからあなたの特定の例:
while(1)
/* noop */;
制御式は定数式であるため、最適化には公平なゲームではありません。
UBとしての無限ループ
では、なぜコンパイラが上記の例外を除いて無限ループを最適化できるのでしょうか。ハンスベームは、無限ループを未定義の動作にする理由を次のように提供しています。N1528:なぜ無限ループの未定義の動作なのか。、次の引用は、関連する問題について良い感じを与えます:
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にアクセスまたは更新する別のスレッドと並行して実行できます。これは、無限ループにもかかわらずcount2にアクセスする変換バージョンでは安全ではなくなりました。したがって、変換によってデータの競合が発生する可能性があります。
このような場合、コンパイラがループの終了を証明できる可能性はほとんどありません。qが非循環リストを指していることを理解する必要があります。これは、ほとんどの主流コンパイラの能力を超えており、プログラム全体の情報がないと不可能なことがよくあります。
C99
C99にはこれが刻まれていないため、基本的にコンパイラーはプログラムの観察可能な動作をエミュレートするだけでよいというセクションで説明されているas-ifルールに5.1.2.3
注目します。要件は、次のとおりです。
準拠する実装の最小要件は次のとおりです。
- シーケンスポイントでは、以前のアクセスが完了し、後続のアクセスがまだ発生していないという意味で、揮発性オブジェクトは安定しています。
- プログラムの終了時に、ファイルに書き込まれるすべてのデータは、抽象セマンティクスに従ってプログラムを実行した場合の結果と同じでなければなりません。
- 対話型デバイスの入力および出力のダイナミクスは、7.19.3で指定されているように実行されるものとします。これらの要件の目的は、プログラムが入力を待機する前にプロンプトメッセージが実際に表示されるように、バッファなしまたは行バッファの出力ができるだけ早く表示されるようにすることです。
これを厳密に読み取ると、実装で無限ループを最適化できるようになります。無限ループを最適化すると、観察可能な動作に変化が生じるシナリオを確実に思いつくことができます。
while(1) ;
printf( "hello world\n" ) ;
多くの人が、プロセスの終了に影響を与えることも観察可能な振る舞いであると主張するでしょう。この立場は、Cコンパイラがフェルマーの最終定理を反証することで採用されています。
コンパイラには、Cプログラムの実装方法にかなりの自由が与えられますが、その出力は、標準で説明されている「C抽象マシン」によって解釈された場合とプログラムが外部から見える動作と同じである必要があります。多くの知識のある人(私を含む)は、プログラムの終了動作を変更してはならないと言っているとこれを読んでいます。明らかに、一部のコンパイラ作成者は同意しないか、そうでなければそれが重要であるとは信じていません。合理的な人々が解釈に同意しないという事実は、C標準に欠陥があることを示しているように思われます。
アップデート
私はどういうわけか、上記の記事のフォローアップ、コンパイラと終了の再検討を見逃しました。これは、セクションについて次のように述べています5.1.2.3
。
2番目の要件はトリッキーなものです。抽象マシンで実行されているプログラムの終了について話している場合、プログラムが終了しないため、空虚に満たされます。コンパイラによって生成された実際のプログラムの終了について話している場合、ファイルに書き込まれるデータ(stdoutはファイル)が抽象マシンによって書き込まれるデータと異なるため、Cの実装にはバグがあります。(この読みはハンス・ベームによるものです。私はこの微妙な点を標準から外すことができませんでした。)
空のループを削除できるようにするためにC11でカーブを作成する必要があるということは、これが以前は許容できる最適化ではなかったことを意味するという、より弱い議論をすることもできます。
これは無限のgotoループにも当てはまりますか?
これは、無限のgotoループにも当てはまるという意図があると思います。C ++では、セクション1.10
[intro.multithread]に次のように記載されているため、これは明らかに当てはまります。
実装では、任意のスレッドが最終的に次のいずれかを実行すると想定する場合があります
- 終了、
- ライブラリI/O関数を呼び出します。
- 揮発性オブジェクトにアクセスまたは変更する、または
- 同期操作またはアトミック操作を実行します。
そして、で表現されている意図N1528
は、CとC++の標準が一致することです。
コンパイラのバックエンドは通常、CコンパイラとC ++コンパイラの間で共有されるため、WG14とWG21が採用されるソリューションに同意することが最も重要であるように思われます。別の方法は、バックエンドによる2つの言語の特別な処理、またはCコードの処理時に最適化を無効にすることです。どちらもまったく望ましくないようです。
そして最後に言う:
WG21は、扱いを一貫させるための改善された表現を検討しています。うまくいけば、WG14は結果として生じる変更を追跡します。
5.1.2.4
現在、C11標準には、マルチスレッド実行とデータ競合のセクションに同様の表現が含まれていませんが、コンパイラN1528
が無限のgotoループをCおよびC++では未定義の動作として扱うと想定するのが賢明です。
こちらのUSコメント38と、この変更が適用された論文であるN3196も参照してください。