volatileキーワードについて読むと...

CおよびC ++にはvolatileキーワードがあり、対応するメモリ領域の値はいつでも変更できるため、この領域へのアクセスを最適化できないことをコンパイラに伝えます。 通常、キーワードの説明は、別のスレッド、ハードウェア、またはオペレーティングシステムからいつでも変更できるデータの例をすぐに提供します。 例の説明を読んだ後、大多数の読者は深くあくびをし、この人生でこれを必要としないと判断し、次のセクションに進みます。



今日では、 volatileキーワードを使用するためのあまり珍しいシナリオを検討します。







C ++標準では、いわゆる観測可能な動作を、 volatile (1.9 / 6)として宣言されたデータの入出力および読み取り/書き込み操作のシーケンスとして定義しています。 観察された動作を維持する範囲内で、コンパイラは必要に応じてコードを最適化できます。



たとえば...あなたのコードはオペレーティングシステムを使用してメモリを割り当て、オペレーティングシステムが要求された領域全体に物理メモリページを割り当てるようにします。 多くのオペレーティングシステムは、最初の実際の使用時にページを割り当てます。これにより、追加の遅延が発生する可能性があります。たとえば、これらの遅延を回避し、以前のポイントに転送する必要があります。 次のようなコードを書くことができます。



for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) { *ptr; }
      
      





このコードは領域全体で実行され、メモリの各ページから1バイトを読み取ります。 1つの問題-コンパイラはこのコードを最適化し、完全に削除します。 完全な権利を持っている-このコードは、観察された動作に影響を与えません。 オペレーティングシステムによるページの割り当てとこれによる遅延に関する懸念は、観察された動作には当てはまりません。



何をすべきか、何をすべきか...ああ、確かに! コンパイラがこのコードを最適化することを禁止しましょう。



 #pragma optimize( "", off ) for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) { *ptr; } #pragma optimize( "", on )
      
      





さて、結果として...



1. #pragmaを使用しているため、コードの移植性が低くなり、さらに...

2.最適化は完全にオフになり、これによりマシンコードの量が3倍増加します。たとえば、Visual C ++では、この#pragmaは関数の外部でのみ使用できるため、このコードを呼び出しコードに埋め込む必要はありません。



volatileキーワードはここで素晴らしい仕事をします:



 for( volatile char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) { *ptr; }
      
      





そして、それだけです。まさに望みどおりの効果が得られます-コードは、コンパイラに、指定されたステップで必ず読み取るように指示します。 読み取りシーケンスは観測された動作を参照するようになったため、コンパイラによる最適化にはこの動作を変更する権利はありません。



ここで、セキュリティとパラノイアの名前でメモリを書き換えてみましょう(これはナンセンスではありません。 それが実際の生活で起こる方法です)。 その投稿では、特定のマジック関数SecureZeroMemory()が言及されています。これは、指定されたメモリ領域をゼロで上書きすることが保証されているはずです。 たとえば、 memset()または自分で作成した同等のループを使用する場合、次のようになります。



 for( size_t index = 0; index < size; index++ ) ptr[index] = 0;
      
      







ローカル変数の場合、つまり、ループは観察された動作に影響を与えないため、コンパイラがこのループを削除するリスクがあります(その投稿の引数は観察された動作にも適用されません)。



何をすべきか、何をすべきか...ああ、コンパイラーを「だます」...ここでは、「memsetの最適化を防ぐ」というクエリで見つけることができます。



1.ローカル変数を動的メモリ内の変数で置き換え、結果として生じるすべてのオーバーヘッドとリークのリスクを伴います( linux-kernelディストリビューションアーカイブ内のメッセージ

2.アセンブリマジックを使用したマクロ( linux-kernelメーリングリストアーカイブ内のメッセージ

3.インプレースmemset()埋め込みを禁止し、コンパイラの最適化を困難にする特別なプリプロセッサシンボルを使用する提案(もちろん、この機能は使用するライブラリのバージョンでサポートする必要があり、さらにVisual C ++ 10は埋め込み不可としてマークされた関数のコードを最適化することもできます)

4.グローバル変数を使用したさまざまな読み取り/書き込みシーケンス(コードが著しく大きくなり、そのようなコードはスレッドセーフではありません)

5.書き込まれた間違ったデータが読み取られた場合、エラーメッセージを伴う後続の読み取り(コンパイラは、「間違った」データがないことを認識し、このコードを削除する権利を持っています)



これらすべての方法には多くの共通の特徴があります-許容度が低く、検証が困難です。 たとえば、あるバージョンのコンパイラを「トリック」し、新しいバージョンではよりインテリジェントなアナライザーを使用します。このアナライザーは、コードが意味をなさないと推測して削除し、どこでも実行しませんが、一部の場所でのみ実行します。



rewrite関数を別の翻訳単位にコンパイルして、コンパイラが何をするのかを「見えないようにする」ことができます。 コンパイラの次の変更後、リンカコード生成がゲームに入ります(Visual C ++のLTCG、gccのLTO、または使用するコンパイラで呼び出される)-コンパイラは、メモリの書き換えが「意味をなさない」ことを明確に確認し、それを削除します。



コンパイラに嘘をつくことができないという言葉が登場したのも不思議ではありません。



しかし、 SecureZeroMemory()の典型的な実装を見るとどうでしょうか? これは本質的にこれです:



 volatile char *volatilePtr = static_cast<volatile char*>(ptr); for( size_t index; index < size; index++ ) * volatilePtr = 0; }
      
      





そしてそれだけです-コンパイラはレコードを削除する権利を失います...



非常に予期せぬ...すべての迷信に反して、 上記の取り消し線の付いた文は誤りです。



実際には-持っています。 標準では、読み取り/書き込みシーケンスはvolatile修飾子を持つデータに対してのみ保存する必要があるとしています。 そのような場合:



 volatile buffer[size];
      
      





データ自体に揮発性修飾子がなく、このデータへのポインターに揮発性修飾子が追加されている場合、このデータの読み取りと書き込みは、観察された動作に適用されなくなります。



 buffer[size]; SecureZeroMemory(buffer, sizeof(buffer));
      
      





コンパイラ開発者にとってのすべての希望-現在、Visual C ++とgccの両方は、 揮発性修飾子を持つポインタを介したメモリアクセスを最適化しません-これはそのようなポインタを使用するための重要なシナリオの1つです



このデータを持つ変数にvolatile修飾子がない場合、 SecureZeroMemory()と同等の関数でデータを上書きするための標準的な保証方法はありません。 同様に、投稿の最初にメモリが読み込まれることをコードが保証することは不可能です。 すべての可能なソリューションは完全に移植可能ではありません。



これの理由はありふれている-それは「必要ではない」。



書き込まれるデータを含む変数が範囲外になり、その変数が占有するメモリが別の変数に再利用され、予備初期化なしで新しい変数から読み取られる状況は、未定義の動作を指します。 この規格では、このような場合、いかなる動作も許容されると明記されています。 通常、このメモリに以前に書き込まれた「ガベージ」は単に読み取られます。



したがって、標準の観点から、スコープを出る前にそのような変数を確実に書き換えることは意味がありません。 同様に、メモリを読み取るためにメモリを読み取ることは意味がありません。



揮発性ポインターの使用は、おそらく問題を解決する最も効率的な方法です。 まず、コンパイラ開発者は通常、メモリアクセスの最適化を意図的にオフにします。 第二に、オーバーヘッドは最小限です。 第三に、このメソッドが特定の実装で動作するかどうかを確認するのは比較的簡単です。この投稿から上記の些細な例で生成されるマシンコードの種類を見てください。



volatile-ドライバーとオペレーティングシステムだけではありません。



ドミトリー・メッシェリャコフ、

開発者製品部門



All Articles