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


153

更新されました、以下を参照してください!

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関数を呼び出さない。
  • 揮発性オブジェクトにアクセスまたは変更しない、および
  • 同期操作(1.10)またはアトミック操作(29節)を実行しない

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

編集:

この洞察に満ちた記事は、その標準テキストについて述べています

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

それは正しいですか、コンパイラは上記のプログラムの「バイ」を出力することを許可されていますか?


ここにはさらに洞察に満ちたスレッドがあります。これはCへの類似の変更に関するもので、上記のリンクされた記事を書いた人によって開始されました。他の有用な事実の中で、それらはC ++ 0xにも適用されると思われる解決策を提示します(更新:これはn3225ではもう機能しません-下記を参照してください!)

endless:
  goto endless;

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

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


2011年3月31日にn3225で更新:委員会はテキストを1.10 / 24に移動し、

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

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

gotoトリックはなりませんもう仕事します!


4
while(1) { MyMysteriousFunction(); }その不思議な機能の定義を知らなくても独立してコンパイルできなければなりませんよね?それで、ライブラリI / O関数を呼び出すかどうかをどのように判断できますか?言い換えれば、最初の箇条書きを確実にフレーズできるのは、関数を呼び出さないということです
Daniel Earwicker、2010

19
@ダニエル:関数の定義にアクセスできる場合、多くのことが証明できます。手続き間の最適化などがあります。
Potatoswatter 2010

3
現在、C ++ 03では、コンパイラは変更を許可されています int x = 1; for(int i = 0; i < 10; ++i) do_something(&i); x++;にはfor(int i = 0; i < 10; ++i) do_something(&i); int x = 2;?または、おそらく逆に、ループの前xに初期化され2ます。これはdo_something、の値を気にしないので、無限ループに陥るようにの値が変化しない場合x、完全に安全な最適化です。 do_somethingi
Dennis Zickefoose、2010

4
これはmain() { start_daemon_thread(); while(1) { sleep(1000); } }、デーモンをバックグラウンドスレッドで実行するのではなく、すぐに終了する可能性があることを意味しますか?
Gabe

2
「この洞察に満ちた記事」では、明確な定義済みの動作がないため、特定の動作が未定義の動作であると想定しています。それは誤った仮定です。一般に、標準が有限数の動作を開いたままにする場合、実装はそれらのいずれかを選択する必要があります(未指定動作)。これは確定的である必要はありません。何もしないループが終了するかどうかは、おそらくブール値の選択です。するかしないかのどちらかです。他のことをすることは許可されていません。
MSalters 2010

回答:


33

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

はい、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にアクセスまたは更新する別のスレッドと並行して実行できます。これは、無限ループにもかかわらずcount2にアクセスする変換バージョンではもはや安全ではありません。したがって、変換によってデータ競合が発生する可能性があります。

このような場合、コンパイラーがループの終了を証明できる可能性はほとんどありません。qが非循環リストを指していることを理解する必要があります。これは、ほとんどの主流のコンパイラーの能力を超えており、プログラム全体の情報なしでは不可能であることが多いと思います。

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

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

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

または

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

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

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

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


1
現在の言語によって促進され、「ループの終了がオブジェクトの状態に依存する場合、ループの実行に必要な時間は、そのような時間がたまたま無限であったとしても、観察可能な副作用」与えられたdo { x = slowFunctionWithNoSideEffects(x);} while(x != 23);に依存しないだろうループが後に巻き上げコードx、安全かつ合理的と思われるが、コンパイラが想定することができx==23、このようなコードにすると便利よりも危険と思われます。
スーパーキャット2018年

47

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

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

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

編集:ダムの例:

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

ここでは、1つのスレッドがcomplex_io_operation他のスレッドでループ内のすべての複雑な計算を実行している間、その方が高速です。しかし、引用した句がない場合、コンパイラは最適化を行う前に2つのことを証明する必要があります。1)complex_io_operation()ループの結果に依存しないこと、および2)ループが終了することです。証明1)はかなり簡単で、証明2)は停止の問題です。この句を使用すると、ループが終了し、並列化の勝利が得られると想定できます。

また、設計者が本番用コードで無限ループが発生するケースは非常にまれであり、通常、何らかの方法でI / Oにアクセスするイベント駆動型ループのようなものであると考えたと想像します。その結果、彼らはより一般的なケース(非無限、しかし機械的に非無限ループを証明するのは難しい)を最適化するために、まれなケース(無限ループ)を悲観的にしました。

ただし、学習例で使用される無限ループは結果として影響を受け、初心者コードで問題が発生することを意味します。これは完全に良いことだとは言えません。

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

同様の引数を検討してください。「コンパイラは、変数xがシーケンスポイント間で最大1回だけ割り当てられると想定する場合があります」は、「シーケンスポイント間でxに複数回割り当てることは未定義です」と同等です。


「証明1)はかなり簡単です」-実際には、コンパイラがJohannesが尋ねている節の下でループ終了を想定できるようにするために、3つの条件の直後に続きませんか?私はそれらは「おそらく永久にスピンすることを除いて、ループには目に見える効果はない」とみなされ、この条項は「永久にスピンする」がそのようなループの動作を保証しないことを保証します。
スティーブジェソップ2010

@Steve:ループが終了しない場合は簡単です。しかし、ループが終了した場合、それはの処理に影響する重要な動作をする可能性がありcomplex_io_operationます。
フィリップポッター

おっと、はい、IO操作で使用されている不揮発性のローカル/エイリアス/何でも変更する可能性があることを逃しました。つまり、必ずしもそうではありませんが、コンパイラがそのような変更が発生しないことを証明できる場合が多くあります。
スティーブジェソップ2010

「しかし、これは、例の学習で使用される無限ループが結果として影響を受け、初心者のコードで問題が発生することを意味します。これが完全に良いことであるとは言えません。」最適化をオフにしてコンパイルするだけで機能します
KitsuneYMG

1
@supercat:あなたが説明することは実際に起こることですが、それはドラフト標準が要求するものではありません。コンパイラーがループが終了するかどうかを知らないとは想定できません。コンパイラー、ループが終了しないことを知っている場合、好きなように実行できます。DS9Kは、 あろうため、鼻悪魔作成任意のないI / O等を有する無限ループを(したがって、DS9Kが停止問題を解決する。)
フィリップ・ポッター

15

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

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

無限ループがUBの場合、それは非終了プログラムが意味がないと見なされていることを意味します。C++ 0xによると、それらはセマンティクスがありません。

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


7
「空の無限ループは未定義の動作です」?アラン・チューリングは違うことを懇願するでしょう、しかし彼が彼の墓で回転を乗り越えたときだけです。
ドナルフェロー2010

11
@Donal:Turingマシンでのセマンティクスについては何も言わなかった。C ++では、副次的影響のない無限ループのセマンティクスについて説明します。そして私がそれを読んだとき、C ++ 0xはそのようなループは未定義であると言うことを選択します。
jalf

空の無限ループはばかげているので、それらに特別なルールを設ける理由はありません。このルールは、無制限の(できれば無限ではない)期間の有用なループを処理するように設計されており、将来必要になるがすぐには必要としないものを計算します。
supercat

1
これは、C ++ 0xが組み込みデバイスに適していないことを意味しますか?ほとんどすべての組み込みデバイスは非終端であり、大きな脂肪の中で仕事をしwhile(1){...}ます。それらはwhile(1);、ウォッチドッグ支援リセットを呼び出すために日常的に使用します。
vsz 2014年

1
@vsz:最初のフォームは問題ありません。無限ループは、何らかの監視可能な動作がある限り、完全に明確に定義されています。2番目の形式はよりトリッキーですが、私は2つの非常に簡単な方法を考えることができます:(1)組み込みデバイスをターゲットとするコンパイラーは、その場合により厳密な動作を定義することを選択できます、または(2)ダミーライブラリ関数を呼び出すボディを作成します。コンパイラがその関数が何をするかを知らない限り、それはそれが何らかの副作用を持っているかもしれないと仮定しなければならないので、ループを混乱させることはできません。
jalf

8

これは、このタイプの質問に沿っていると思います。スレッドます。最適化により、空のループが削除される場合があります。


3
いい質問です あの男は、この段落でコンパイラが引き起こす問題を正確に抱えていたようです。回答の1つによるリンクされたディスカッションでは、「残念ながら、「未定義の動作」という言葉は使用されていません。しかし、規格が「コンパイラはPを想定しているかもしれない」と述べているときはいつでも、プロパティnot-Pには未定義のセマンティクスがあります。」。これには驚きました。これは、上記のサンプルプログラムの動作が未定義であり、どこからともなくsegfaultする可能性があることを意味しますか?
Johannes Schaub-litb

@Johannes:「想定される可能性があります」というテキストは、私が提出しなければならないドラフトの他の場所では発生せず、「想定される可能性があります」は数回しか発生しません。改行して一致しない検索機能で確認しましたが、見落としている可能性があります。したがって、著者の一般化が証拠に基づいて正当化されるかどうかはわかりません、数学者として、私は引数の論理を認めなければなりません。コンパイラが何かを偽であると仮定すると、一般的に何でも推測できるかもしれません...
スティーブジェソップ

...コンパイラについてのプログラムに関する推論の矛盾を許可すると、特にUBで非常に強く示唆されます。これは、特に、任意のXについて、コンパイラがプログラムがXと同等であると推論できるようにするためです。それを許可します。私も作者に同意します。UBが意図されている場合、それは明示的に記述されるべきであり、意図されていない場合は、仕様テキストが間違っていて、修正される必要があります(おそらく、仕様言語の同等のものによって、「コンパイラが効果のないコードでループします」とはわかりません)。
スティーブジェソップ2010

@SteveJessop:コードの一部(無限ループを含む)の実行は、コードの一部が監視可能なプログラムの動作に影響を与えるような時間まで延期される可能性があると単純に言ってどう思いますか?ルール、コードの実行に必要な時間は、たとえ無限であっても、「観察可能な副作用」ではありません。ある特定の値を保持する変数なしではループが終了できないことをコンパイラーが示すことができる場合、その値を保持してループが終了できなかったことが示されていても、変数はその値を保持していると見なされる場合があります。
スーパーキャット2014年

@supercat:あなたがその結論を述べたように、私はそれが物事を改善するとは思いません。ループが何らかの形でオブジェクトXとbit-pattern xに対して存在しないと思われる場合、コンパイラーはXbit-pattern を保持せずにループが存在しないことを示すことができますx。それは空虚に真実です。したがってX、任意のビットパターンを保持していると見なすことができます。これは、間違っXxいるとすぐに原因となるという意味で、UBと同じくらい悪いです。だから私はあなたがあなたの標準でもっと正確にする必要があると信じています。無限ループの「終わり」で何が起こるかについて話し、それをある有限演算と同等に示すことは困難です。
スティーブジェソップ2014年

8

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

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

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");

2
@ JohannesSchaub-litb:ループが実行中に揮発性変数を読み取ったり書き込んだりせず、その可能性のある関数を呼び出さない場合、コンパイラーはループの任意の部分を自由に延期できます。その中で計算された何かにアクセスする最初の努力。を指定するとunsigned int dummy; while(1){dummy++;} fprintf(stderror,"Hey\r\n"); fprintf(stderror,"Result was %u\r\n",dummy);、1つ目fprintfは実行できますが2つ目は実行できませんでした(コンパイラーdummyは2つのの間の計算を移動できますが、fprintfその値を出力するものを通過できません)。
スーパーキャット2014年

1

この問題はおそらく、「後のコードが前のコードに依存せず、前のコードがシステムの他の部分に影響を及ぼさない場合、コンパイラーの出力前者がループを含む場合でも、前者のコードが実際にいつ完了するかどうかに関係なく、前者の実行の前、後、または混合して、後のコードを実行する可能性があります。たとえば、コンパイラは次のように書き直すことができます。

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( "結果は");
    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(合計);

なるだろう

  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();
  }
  そうしないと
  {
    for(i = 0; num_reps> i; i ++)
      update_progress_bar(i);
    wait_for_other_thread();
  }
  show_result(合計);

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


プログレスバーの表示はライブラリI / O呼び出しであるため、プログレスバーのケースを分離できなかったと思います。最適化はこの方法で目に見える動作を変更するべきではありません。
フィリップポッター、

@フィリップ・ポッター:スロールーチンに副作用があった場合、それは確かに本当でしょう。以前の例では、そうしなければ意味がなかったので、変更しました。仕様の私の解釈は、その影響(実行にかかる時間以外)が見えるようになるまで、つまりshow_result()呼び出しまで、システムが遅いコードの実行を延期することを許可されているということです。プログレスバーコードが現在の合計を利用した場合、または少なくともそうするふりをしている場合は、低速のコードと強制的に同期します。
supercat

1
これは、0から100まで高速で進行し、その後、年齢が経過するとそれらすべてのプログレスバーが説明されることを説明します;)
paulm

0

それが無限ループである場合、それは重要なケースのコンパイラにとって決定的ではありません。

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

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


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

私が聞いた1つの例は、実際には別の方法で解決する必要がある醜いハックでした。リセットをトリガーする唯一の方法は、ウォッチドッグが自動的に再起動するようにデバイスをフリーズすることであった組み込みシステムに関するものでした。

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


1
無限ループが必要になる可能性のある例:パフォーマンス上の理由でスリープしたくない組み込みシステムで、すべてのコードが1つまたは2つの割り込みでハングアップしている場合は、
JCx 2014

標準Cの@JCx割り込みでは、メインループがチェックするフラグを設定する必要があります。そのため、フラグが設定されている場合、メインループには監視可能な動作があります。割り込みで実質的なコードを実行することは、移植不可能です。
MM

-1

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

つまり、グローバルを揮発性にする-ポインタ/参照を介してそのようなループに渡される引数と同様に。


他のスレッドと対話している場合は、それらを揮発性にしたり、アトミックにしたり、ロックで保護したりしないでください。
BCoates 2011

1
Thjsはひどいアドバイスです。それらを作成することvolatileは必要でも十分でもなく、パフォーマンスを劇的に低下させます。
David Schwartz
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.