web-dev-qa-db-ja.com

コンパイラがローカルのvolatile変数を最適化することは許可されていますか?

コンパイラーはこれを最適化することを許可されていますか(C++ 17標準に従って):

int fn() {
    volatile int x = 0;
    return x;
}

これに?

int fn() {
    return 0;
}

はいの場合、なぜですか?そうでない場合は、なぜですか?


このテーマについて考えてみましょう。現在のコンパイラはfn()をスタックに置かれたローカル変数としてコンパイルし、それを返します。たとえば、x86-64では、gccはこれを作成します。

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

さて、私の知る限りでは、標準はローカルのvolatile変数をスタックに置くべきだとは言っていません。したがって、このバージョンも同様に優れています。

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

ここで、edxxを格納します。しかし、今、なぜここで停止するのですか? edxeaxは両方ともゼロなので、次のように言うことができます。

xor    eax,eax // eax is the return, and x as well
ret    

そして、fn()を最適化バージョンに変換しました。この変換は有効ですか?そうでない場合、どのステップが無効ですか?

72
geza

いいえ。volatileオブジェクトへのアクセスは、I/Oとまったく同じように観察可能な動作と見なされ、ローカルとグローバルは特に区別されません。

適合実装の最小要件は次のとおりです。

  • volatileオブジェクトへのアクセスは、抽象マシンのルールに従って厳密に評価されます。

[...]

これらは、プログラムの観察可能な動作と総称されます。

N3690、[intro.execution]、¶8

Howこれは正確に観察可能であり、標準の範囲外であり、I/Oおよびグローバルvolatileオブジェクトへのアクセスとまったく同じように、実装固有の領域に直接分類されます。 volatileは、「あなたはここで起こっていることをすべて知っていると思うが、それはそうではありません。あなたのプログラムであなたのバイトで秘密のことをしているので、私を信頼してこのことをやります」これは実際に[dcl.type.cv]¶7で説明されています:

[注:volatileは、オブジェクトの値が実装によって検出できない手段によって変更される可能性があるため、オブジェクトを含む積極的な最適化を回避するための実装へのヒントです。さらに、一部の実装では、volatileは、オブジェクトにアクセスするために特別なハードウェア命令が必要であることを示す場合があります。詳細なセマンティクスについては、1.9を参照してください。一般に、volatileのセマンティクスは、C++と同じであるように意図されています。

59
Matteo Italia

このループは、観察可能な動作がないため、as-ifルールによって最適化できます。

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

これはできません:

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

2番目のループは、反復ごとに何かを実行します。つまり、ループにはO(n)時間かかります。定数が何であるかはわかりませんが、それを測定することができます。それから、(多かれ少なかれ)既知の時間ループするのに忙しくなります。

規格では、揮発性物質へのアクセスは順番に発生する必要があるとされているため、これを行うことができます。コンパイラがこの場合、標準が適用されないと判断した場合、バグ報告を提出する権利があると思います。

コンパイラーがloopedをレジスターに入れることを選択した場合、それに対する適切な引数はないと思います。ただし、ループの繰り返しごとにそのレジスタの値を1に設定する必要があります。

11
rici

volatileは観測可能なI/Oを意味するという完全な理解にもかかわらず、私は多数意見に異議を唱えます。

このコードがある場合:

{
    volatile int x;
    x = 0;
}

コンパイラcanas-ifルールassumingの下で最適化する

  1. それ以外の場合、volatile変数は、たとえばポインター(指定されたスコープにはそのようなものがないため、ここでは明らかに問題ではありません)

  2. コンパイラは、そのvolatileに外部からアクセスするためのメカニズムを提供しません

理由は単純に、基準#2により、とにかく違いを観察できなかったということです。

ただし、コンパイラーでは、基準#2が満たされない場合があります!コンパイラーmayは、スタックの分析などによって、「外部」からvolatile変数を観察することについて、追加の保証を提供しようとします。そのような状況では、振る舞いは実際にisであるため、離れて最適化することはできません。

さて、問題は、次のコードは上記とは違うのでしょうか?

{
    volatile int x = 0;
}

最適化に関して、Visual C++でこれについて異なる動作を観察したと思いますが、どのような理由があるのか​​完全にはわかりません。初期化は「アクセス」としてカウントされない可能性がありますか?よく分かりません。もし興味があれば、これは別の質問に値するかもしれませんが、そうでなければ、答えは私が上で説明した通りであると思います。

9
Mehrdad

理論的には、割り込みハンドラは

  • 戻りアドレスがfn()関数内にあるかどうかを確認します。インストルメンテーションまたは添付のデバッグ情報を介して、シンボルテーブルまたはソース行番号にアクセスする場合があります。
  • 次に、xの値を変更します。これは、スタックポインターからの予測可能なオフセットに格納されます。

…したがって、fn()はゼロ以外の値を返します。

6
berendi

as-if ルールと volatile キーワードの詳細なリファレンスを追加します。 (これらのページの下部で、「参照」と「参照」に従って元の仕様に戻ってください。しかし、cppreference.comは読みやすく、理解しやすいと思います。)

特に、このセクションを読んでほしい

volatileオブジェクト-タイプがvolatile修飾されたオブジェクト、volatileオブジェクトのサブオブジェクト、またはconst-volatileオブジェクトの可変サブオブジェクト。 volatileで修飾された型のglvalue式を介して行われるすべてのアクセス(読み取りまたは書き込み操作、メンバー関数呼び出しなど)は、最適化の目的で(つまり、実行の単一スレッド内でvolatileアクセスは、揮発性アクセスの前または後のシーケンスである別の目に見える副作用で最適化または再順序付けすることはできません。 )。不揮発性glvalueを介して(たとえば、不揮発性タイプへの参照またはポインターを介して)揮発性オブジェクトを参照しようとすると、未定義の動作が発生します。

したがって、volatileキーワードspecificallyは、 glvalues でのコンパイラ最適化を無効にすることです。ここでvolatileキーワードが影響を与えることができるのはおそらくreturn xだけであり、コンパイラは関数の残りの部分で必要な処理を実行できます。

コンパイラーが戻り値を最適化できる程度は、この場合にコンパイラーがxのアクセスを最適化できる程度に依存します(何も並べ替えを行わず、厳密に言えば、戻り式を削除しないためです。 、しかし、それはスタックへの読み書きであり、それは合理化できるはずです。)私が読んだように、これはコンパイラーが最適化できる範囲で灰色の領域であり、両方の方法で簡単に議論することができます。

サイドノート:これらの場合、コンパイラーがあなたが望んだ/必要なものの反対をすることを常に仮定してください。 (少なくともこのモジュールでは)最適化を無効にするか、必要に応じてより定義された動作を見つけてください。 (これがユニットテストが非常に重要である理由でもあります)それが欠陥であると信じるなら、あなたはそれをC++の開発者と一緒に育てるべきです。


これはまだ非常に読みにくいので、あなたが自分で読むことができるように、関連があると思うものを含めようとしています。

glvalue glvalue式は左辺値またはx値です。

プロパティ:

Glvalueは、lvalue-to-rvalue、array-to-pointer、またはfunction-to-pointerの暗黙的な変換により、暗黙的にprvalueに変換できます。 glvalueはポリモーフィックである場合があります。それが識別するオブジェクトの動的な型は、必ずしも式の静的な型ではありません。式で許可されている場合、glvalueは不完全な型を持つことができます。


xvalue次の式はxvalue式です。

関数呼び出しまたはオーバーロードされた演算子式。戻り値の型は、std :: move(x)などのオブジェクトへの右辺値参照です。 a [n]、組み込み添字式。1つのオペランドは配列右辺値です。 a.m、オブジェクト式のメンバー。ここで、aは右辺値、mは非参照型の非静的データメンバーです。 a。* mp、オブジェクト式のメンバーへのポインター。ここで、aは右辺値であり、mpはデータメンバーへのポインターです。 ? b:c、いくつかのbとcの三項条件式(詳細は定義を参照); static_cast(x)などのオブジェクト型への右辺値参照へのキャスト式。一時的な実体化の後、一時的なオブジェクトを指定する式。 (C++ 17以降)プロパティ:

右辺値と同じ(下)。 glvalue(以下)と同じです。特に、すべての右辺値のように、xvaluesは右辺値の参照にバインドし、すべてのglvaluesのように、xvaluesはポリモーフィックであり、非クラスのxvaluesはcv修飾される場合があります。


左辺値次の式は左辺値式です。

std :: cinやstd :: endlなど、タイプに関係なく、変数、関数、またはデータメンバーの名前。変数の型が右辺値参照であっても、その名前で構成される式は左辺値式です。戻り値の型がstd :: getline(std :: cin、str)、std :: cout << 1、str1 = str2、または++ itなどの関数呼び出しまたはオーバーロードされた演算子式。 a = b、a + = b、a%= b、およびその他のすべての組み込み代入および複合代入式。 ++ aおよび--a、組み込みの事前インクリメントおよび事前デクリメント式。 * p、組み込みの間接式。 a [n]およびp [n]、組み込み添え字式。ただし、aが配列右辺値である場合を除きます(C++ 11以降)。 a.m、オブジェクト式のメンバー。ただし、mはメンバー列挙子または非静的メンバー関数、またはaは右辺値であり、mは非参照型の非静的データメンバーです。 p-> m、ポインター式の組み込みメンバー。ただし、mはメンバー列挙子または非静的メンバー関数です。 a。* mp、オブジェクト式のメンバーへのポインター。ここで、aは左辺値であり、mpはデータメンバーへのポインターです。 p-> * mp、ポインター式のメンバーへの組み込みポインター。ここで、mpはデータメンバーへのポインターです。 a、b、組み込みのコンマ式。bは左辺値です。 ? b:c、いくつかのbとcの3項条件式(たとえば、両方が同じ型の左辺値であるが、詳細は定義を参照); 「Hello、world!」などの文字列リテラル。 static_cast(x)などの左辺値参照型へのキャスト式。関数呼び出しまたはオーバーロードされた演算子式。戻り値の型は関数への右辺値参照です。 static_cast(x)などの関数型への右辺値参照へのキャスト式。 (C++ 11以降)プロパティ:

Glvalue(以下)と同じです。左辺値のアドレスを取得できます:&++ i 1 および&std :: endlは有効な式です。変更可能な左辺値は、組み込み代入演算子および複合代入演算子の左側のオペランドとして使用できます。左辺値を使用して左辺値参照を初期化できます。これにより、式で識別されるオブジェクトに新しい名前が関連付けられます。


as-ifルール

C++コンパイラは、次の条件が満たされている限り、プログラムの変更を実行できます。

1)すべてのシーケンスポイントで、すべての揮発性オブジェクトの値は安定しています(以前の評価が完了し、新しい評価が開始されていません)(C++ 11まで)1)揮発性オブジェクトへのアクセス(読み取りおよび書き込み)は、セマンティクスに従って厳密に行われますそれらが現れる表現の。特に、同じスレッド上の他の揮発性アクセスに関しては並べ替えられません。 (C++ 11以降)2)プログラム終了時、ファイルに書き込まれたデータは、プログラムが書き込まれたとおりに実行されたかのようになります。 3)対話型デバイスに送信されるプロンプトテキストは、プログラムが入力を待機する前に表示されます。 4)ISO Cプラグマ#pragma STDC FENV_ACCESSがサポートされ、ONに設定されている場合、浮動小数点環境への変更(浮動小数点例外および丸めモード)は、浮動小数点算術演算子および関数によって監視されることが保証されます。キャストおよび代入以外の浮動小数点式の結果は、上記の中間結果にもかかわらず、式の型(FLT_EVAL_METHODを参照)とは異なる浮動小数点型の範囲と精度を持つ場合があることを除いて、書かれたまま実行されるかのように呼び出します。任意の浮動小数点式の範囲は、無限の範囲と精度であるかのように計算されます(#pragma STDC FP_CONTRACTがOFFでない場合)


仕様を読みたい場合、これらはあなたが読む必要があるものだと思います

参照資料

C11標準(ISO/IEC 9899:2011):6.7.3型修飾子(p:121-123)

C99標準(ISO/IEC 9899:1999):6.7.3型修飾子(p:108-110)

C89/C90規格(ISO/IEC 9899:1990):3.5.3型修飾子

6
Tezra