web-dev-qa-db-ja.com

「while(1);」を最適化するC ++ 0xで

更新、以下を参照!

C++ 0xを使用すると、コンパイラが次のスニペットの「Hello」を出力できることを聞いて読んだことがあります

#include <iostream>

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

スレッドと最適化機能に関係しているようです。しかし、これは多くの人々を驚かせることができるように見えます。

誰かがこれを許可するために必要だった理由についての良い説明を持っていますか?参考までに、最新のC++ 0xドラフトでは6.5/5

Forステートメントの場合のfor-init-statementの外側のループ。

  • ライブラリI/O関数を呼び出しません。
  • volatileオブジェクトにアクセスしたり、変更したりしない。
  • 同期操作(1.10)またはアトミック操作(29節)を実行しません

実装によって終了すると想定される場合があります。 [注:これは、終了が証明できない場合でも、空のループの削除などのコンパイラー変換を可能にすることを目的としています。 —終了ノート]

編集:

この洞察力に富んだ記事 はその標準テキストについて述べています

残念ながら、「未定義の動作」という言葉は使用されていません。ただし、標準が「コンパイラがPを想定する可能性がある」と言うときはいつでも、not-Pプロパティを持つプログラムが未定義のセマンティクスを持っていることを意味します。

それは正しいですか、コンパイラは上記のプログラムの「バイ」を出力できますか?


さらに洞察に満ちた thread here があります。これは、上記のリンクされた記事を行ったGuyによって始められた、Cの類似の変更についてです。他の有用な事実の中で、C++ 0x(Updateにも適用されると思われる解決策を提示します:これはn3225では動作しません-以下を参照してください!)

endless:
  goto endless;

コンパイラーはそれを最適化することを許可されていません、それはループではなくジャンプだからです。別の男は、C++ 0xとC201Xで提案された変更を要約しています

ループを書くことにより、プログラマーは、ループが目に見える振る舞い(I/Oの実行、揮発性オブジェクトへのアクセス、または同期またはアトミック操作の実行)を行うことをeitherにアサートします。またはは最終的に終了します。副作用のない無限ループを書くことでこの仮定に違反すると、コンパイラーに嘘をつき、プログラムの動作は未定義になります。 (運が良ければ、コンパイラはそれについて警告するかもしれません。)言語は、目に見える振る舞いなしに無限ループを表現する方法を提供しません(もはや提供しませんか?)。


N.3225を使用した3.1.2011の更新:委員会はテキストを1.10/24に移動して発言

実装は、スレッドが最終的に次のいずれかを実行すると想定する場合があります。

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

gotoトリックはnotもう機能しません!

149

誰かがこれを許可するために必要だった理由についての良い説明を持っていますか?

はい、ハンス・ベームは 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は非循環リストを指していることを理解する必要がありますが、これはほとんどの主流コンパイラの能力を超えており、プログラム全体の情報なしでは不可能な場合が多いと思います。

非終了ループによって課せられる制限は、コンパイラが終了を証明できない終了ループの最適化、および実際の非終了ループの最適化に対する制限です。前者は後者よりもはるかに一般的であり、最適化することがより興味深い場合があります。

また、コンパイラーが終了を証明するのが困難な整数ループ変数を持つforループも明らかに存在するため、コンパイラーは6.8.5p6なしでループを再構築することは困難です。でも

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

または

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

扱いにくいようです。 (前者の場合、終了を証明するためにいくつかの基本的な数論が必要です。後者の場合、そうするためにjの可能な値について何かを知る必要があります。 )

この問題は、コンパイラー並列化とキャッシュ最適化変換を含むほぼすべてのループ再構築変換に当てはまるようです。これらの変換はどちらも重要になりそうであり、数値コードにとってはすでに重要です。これは、特に私たちのほとんどが意図的に無限ループを書くことはめったにないので、無限ループを可能な限り最も自然な方法で書くことができるという利点のためにかなりのコストになる可能性があります。

Cとの1つの大きな違いは、 C11は定数式である式を制御するための例外を提供します です。これはC++とは異なり、特定の例をC11で明確に定義します。

29
Shafik Yaghmour

私にとって、関連する正当性は次のとおりです。

これは、終了を証明できない場合でも、空のループの削除などのコンパイラー変換を可能にすることを目的としています。

おそらく、これは終了を機械的に証明することはdifficultであり、終了を証明することができないため、ループの前から後へ、またはその逆に独立した操作を移動するなど、有用な変換を行うコンパイラーが妨げられるためですループが別のスレッドで実行されている間に、1つのスレッドでループ操作を実行します。これらの変換がなければ、1つのスレッドがループを終了するのを待つ間、ループは他のすべてのスレッドをブロックする可能性があります。 (個別のVLIW命令ストリームを含む、あらゆる形式の並列処理を意味するために、「スレッド」を大まかに使用します。)

編集:ダムの例:

_while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);
}
complex_io_operation();
cout << "Results:" << endl;
cout << x << endl;
_

ここでは、一方のスレッドが_complex_io_operation_を実行し、もう一方のスレッドがループ内のすべての複雑な計算を実行する方が高速です。しかし、引用した句がないと、コンパイラは最適化を行う前に2つのことを証明する必要があります。1)complex_io_operation()はループの結果に依存しないこと、2)ループが終了すること。証明1)は非常に簡単で、証明2)は停止する問題です。この句を使用すると、ループが終了し、並列化が優先されると見なされます。

また、設計者は、プロダクションコードで無限ループが発生するケースは非常にまれであり、通常は何らかの方法でI/Oにアクセスするイベントドリブンループのようなものだと考えたと思います。その結果、彼らは、より一般的なケース(非無限、ただし機械的に非無限ループを証明するのが難しい)を最適化することを支持して、まれなケース(無限ループ)を悲観的に見ています。

ただし、学習例で使用される無限ループは結果として苦しみ、初心者コードで落とし穴が生じることを意味します。これが完全に良いことだとは言えません。

編集:あなたが今リンクしている洞察力のある記事に関して、私は「コンパイラがプログラムについてXを仮定するかもしれない」は論理的に「プログラムがXを満たさない場合、振る舞いは未定義」と言うでしょう。これを次のように示すことができます。プロパティXを満たさないプログラムが存在するとします。このプログラムの動作はどこで定義されますか?標準では、プロパティXがtrueであると仮定した場合の動作のみを定義しています。標準は、動作を未定義として明示的に宣言していませんが、省略により未定義と宣言しています。

同様の議論を考えてみましょう:「コンパイラーは、変数xがシーケンスポイント間で最大1回しか割り当てられないと想定するかもしれません」は、「シーケンスポイント間でxに複数回割り当てることは未定義」と同等です。

47
Philip Potter

正しい解釈はあなたの編集によるものだと思います:空の無限ループは未定義の動作です。

特に直感的な動作だとは言いませんが、この解釈は、コンパイラーがUBを呼び出さずにignore無限ループを任意に許可されるという、代替解釈よりも理にかなっています。

無限ループがUBである場合、それは単に、終了しないプログラムが意味のあるものと見なされないことを意味します。C++ 0xによれば、それらはhave意味論なしです。

それにもある程度の意味があります。これらは特別なケースであり、多くの副作用が発生しなくなり(たとえば、mainから何も返されない)、無限ループを保持しなければならないため、多くのコンパイラ最適化が妨げられます。たとえば、ループに副作用がない場合、ループ全体で計算を移動することは完全に有効です。最終的には、いずれにしても計算が実行されるためです。しかし、ループが終了しない場合、コードを安全に再配置することはできません。なぜなら、mightは、プログラムがハングする前に実際に実行される操作を変更しているからです。ハングしているプログラムをUBとして扱わない限り、そうです。

14
jalf

関連する問題は、副作用が競合しないコードを並べ替えることがコンパイラに許可されていることです。コンパイラが無限ループ用の非終了マシンコードを生成した場合でも、驚くべき実行順序が発生する可能性があります。

これは正しいアプローチだと思います。言語仕様は、実行の順序を強制する方法を定義します。順序を変更できない無限ループが必要な場合は、次のように記述します。

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");
8
Daniel Newby

これは、このタイプの question の行に沿っていると思います。これは別の thread を参照します。最適化により、空のループが削除されることがあります。

8
linuxuser27

「後のコードが前のコードに依存せず、前のコードがシステムの他の部分に副作用を持たない場合、コンパイラの出力は前者にループが含まれている場合でも、前者の実行前、後、または前者と混合して、後のコードを実行できます。 前のコードが実際に完了するかどうか、または関係なく。たとえば、コンパイラは次のように書き換えることができます。

 void testfermat(int n)
 {
 int a = 1、b = 1、c = 1; 
 while(pow(a、n)+ pow (b、n)!= pow(c、n))
 {
 if(b> a)a ++; else if(c> b){a = 1; b ++};その他{a = 1; b = 1; c ++}; 
} 
 printf( "結果は"); 
 printf( "%d /%d /%d"、a、b、c); 
} 

なので

 void testfermat(int n)
 {
 if(fork_is_first_thread())
 {
 int a = 1、b = 1、c = 1; 
 while(pow(a、n)+ pow(b、n)!= pow(c、n))
 {
 if(b> a)a ++; else if(c> b){a = 1; b ++};その他{a = 1; b = 1; c ++}; 
} 
 signal_other_thread_and_die(); 
} 
 else // 2番目のスレッド
 {
 printf( "The result "); 
 wait_for_other_thread(); 
} 
 printf("%d /%d /%d "、a、b、c); 
} 

一般的には不合理ではありませんが、私は心配するかもしれません:

 int total = 0; 
 for(i = 0; num_reps> i; i ++)
 {
 update_progress_bar(i); 
 total + = do_something_slow_with_no_side_effects(i); 
} 
 show_result(total); 

になるだろう

 int total = 0; 
 if(fork_is_first_thread())
 {
 for(i = 0; num_reps> i; i ++)
 total + = do_something_slow_with_no_side_effects(i); 
 signal_other_thread_and_die(); 
} 
 else 
 {
 for(i = 0; num_reps> i; i ++) 
 update_progress_bar(i); 
 wait_for_other_thread(); 
} 
 show_result(total); 

1つのCPUで計算を処理し、別のCPUで進行状況バーの更新を処理することにより、書き換えにより効率が向上します。残念ながら、プログレスバーの更新は本来よりも有用性が低くなります。

1
supercat

コンパイラが無限ループである場合、自明ではないケースについては決定できません。

場合によっては、オプティマイザーがコードのより複雑なクラスに到達することがあります(たとえば、O(n ^ 2)で、O(n)またはO(1)最適化後)。

そのため、C++標準に無限ループを削除することを禁止するようなルールを含めると、多くの最適化が不可能になります。そして、ほとんどの人はこれを望んでいません。これはあなたの質問にかなり答えていると思います。


別のこと:何もしない無限ループが必要な有効な例を見たことがない。

私が聞いた1つの例は、本当にそうでなければ解決されるべきいハックでした:リセットをトリガーする唯一の方法は、ウォッチドッグが自動的に再起動するようにデバイスを凍結することである組み込みシステムに関するものでした.

何もしない無限ループが必要な有効/良い例を知っているなら、教えてください。

0
Albert

不揮発性、非同期変数を介して他のスレッドと対話するという事実を除き、無限ループになるループは、新しいコンパイラーで不正な動作を引き起こす可能性があることを指摘する価値があると思います。

つまり、グローバルを揮発性にするだけでなく、ポインター/参照を介してこのようなループに引数を渡します。

0
spraff