なぜCではvolatile
が必要なのですか?それは何のために使われますか?それは何をしますか?
Volatileは、volatile変数と関係があるものを最適化しないようにコンパイラーに指示します。
それを使用する唯一の理由があります:あなたがハードウェアとインターフェースするとき。
RAMのどこかにマップされ、コマンドポートとデータポートの2つのアドレスを持つ小さなハードウェアがあるとしましょう。
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
今、あなたはいくつかのコマンドを送りたいです。
void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
簡単に見えますが、データとコマンドの書き込み順序をコンパイラが自由に変更できるため、失敗する可能性があります。これにより、私たちの小さなガジェットは前のデータ値を使ってコマンドを発行することになります。また、待機中のビジーループを見てください。それは最適化されます。コンパイラは賢いことを試み、isbusyの値を一度だけ読んでから無限ループに入ります。それはあなたが望むものではありません。
これを回避する方法は、ポインタガジェットをvolatileとして宣言することです。このように、コンパイラはあなたが書いたことをすることを強いられます。メモリ割り当てを削除することはできず、レジスタに変数をキャッシュすることも、割り当ての順序を変更することもできません。
これは正しいバージョンです。
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
Cのvolatile
は、実際には変数の値を自動的にキャッシュしないことを目的として生まれました。この変数の値をキャッシュしないようにマシンに指示します。そのため、遭遇するたびにメインメモリから与えられたvolatile
変数の値を取ります。このメカニズムが使用されるのは、値がOSまたは割り込みによっていつでも変更される可能性があるためです。そのため、volatile
を使用すると、毎回値にアクセスするのに役立ちます。
volatile
の別の用途はシグナルハンドラです。このようなコードがあるならば:
quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
コンパイラは、ループ本体がquit
変数に触れず、ループをwhile (true)
ループに変換しないことに気付くことができます。たとえquit
変数がSIGINT
とSIGTERM
のシグナルハンドラに設定されていても。コンパイラはそれを知る方法がありません。
ただし、quit
変数がvolatile
と宣言されている場合、コンパイラは毎回それをロードするように強制されます。これはまさにこの状況であなたが望むものです。
volatile
は、変数にアクセスしているコード以外の方法で変数が変更される可能性があることをコンパイラに伝えます。例えば、それはI / Oマップメモリ位置であり得る。そのような場合にこれが指定されていない場合、いくつかの変数アクセスは最適化されることができ、例えばその内容はレジスタに保持されることができ、メモリ位置は再び読み返されない。
Andrei Alexandrescuによるこの記事を参照してください、 " volatile - マルチスレッドプログラマの親友 "
の 揮発性 keywordは、特定の非同期イベントが発生した場合にコードが不正確になる可能性があるコンパイラの最適化を防ぐために考案されました。たとえば、プリミティブ変数を次のように宣言したとします。 揮発性コンパイラがそれをレジスタにキャッシュすることは許可されていません。その変数が複数のスレッド間で共有されている場合は悲惨なことになる一般的な最適化です。したがって、原則として、複数のスレッド間で共有しなければならないプリミティブ型の変数がある場合は、それらの変数を宣言します。 揮発性。しかし、実際にはこのキーワードでもっと多くのことができます。スレッドセーフではないコードをキャッチするためにそれを使用することができ、コンパイル時にそれを行うことができます。この記事ではその方法を説明します。このソリューションにはシンプルなスマートポインタが含まれているため、重要なコードセクションのシリアル化も容易になります。
この記事はC
とC++
の両方に当てはまります。
Scott MeyersとAndrei Alexandrescuによる記事「 C++と二重チェックロッキングの危険 」も参照してください。
そのため、いくつかのメモリ位置(例:メモリマップされたポートまたはISR [割り込みサービスルーチン]によって参照されるメモリ)を扱うとき、いくつかの最適化は中断されなければなりません。特に、(1)揮発性変数の内容が「不安定」(コンパイラにとって未知の方法で変更される可能性がある)、(2)揮発性データへのすべての書き込みが「観察可能」であるため(3)揮発性データに対するすべての操作は、ソースコードに現れる順序で実行されます。最初の2つの規則は適切な読み書きを保証します。最後のものは入力と出力を混在させるI/Oプロトコルの実装を可能にします。これは、CおよびC++の不安定性が保証するもので非公式です。
私の簡単な説明は:
いくつかのシナリオでは、ロジックまたはコードに基づいて、コンパイラは変更しないと考える変数の最適化を行います。 volatile
キーワードは、変数が最適化されるのを防ぎます。
例えば:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
上記のコードから、コンパイラはusb_interface_flag
が0として定義されていると考え、whileループでは永遠に0になると考えます。最適化の後、コンパイラはそれを常にwhile(true)
として扱い、無限ループになります。
このようなシナリオを回避するために、フラグをvolatileとして宣言し、この値は外部インタフェースまたはプログラムの他のモジュールによって変更される可能性があることをコンパイラに伝えます。これがvolatileのユースケースです。
Volatileの限界的な使い方は次のとおりです。関数f
の数値微分を計算したいとしましょう。
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
問題は、丸め誤差が原因でx+h-x
が一般的にh
と等しくないことです。考えてみましょう。非常に近い数値を引き算すると、微分の計算を台無しにする可能性のある有効数字がたくさん失われます(1.00001 - 1と思います)。考えられる回避策は次のとおりです。
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
ただし、プラットフォームやコンパイラの切り替えによっては、その関数の2行目が積極的に最適化されたコンパイラによって消去されることがあります。だからあなたは代わりに書く
volatile double hh = x + h;
hh -= x;
コンパイラにhhを含むメモリ位置を強制的に読み込ませ、最終的な最適化の機会を失うこと。
用途は2つあります。これらは特に組み込み開発で頻繁に使用されます。
Volatileキーワードで定義された変数を使用する関数をコンパイラが最適化しない
揮発性メモリは、RAM、ROMなどの正確なメモリ位置にアクセスするために使用されます。
アセンブリリストの例を参照してください。 再:組込み開発におけるCの "volatile"キーワードの使用法
揮発性は、特定のコードシーケンスを最適化しないようにコンパイラに強制する場合にも便利です(たとえば、マイクロベンチマークを記述するため)。
揮発性物質が重要な別のシナリオについて説明します。
より高速なI/Oのためにファイルをメモリマップし、そのファイルが舞台裏で変更される可能性があるとします(たとえば、ファイルはローカルハードドライブにはなく、代わりに別のコンピュータによってネットワーク経由で提供されます)。
(ソースコードレベルで)不揮発性オブジェクトへのポインタを介してメモリマップファイルのデータにアクセスすると、コンパイラによって生成されたコードは、意識することなく同じデータを複数回フェッチする可能性があります。
そのデータが変更されると、プログラムは2つ以上の異なるバージョンのデータを使用するようになり、矛盾した状態になる可能性があります。これは、プログラムの論理的に不正確な動作だけでなく、信頼できないファイルまたは信頼できない場所からのファイルを処理する場合に悪用される可能性があるセキュリティホールにつながる可能性があります。
セキュリティに関心があり、そうすべきなら、これは考慮すべき重要なシナリオです。
揮発性とは、ストレージがいつでも変更され、変更される可能性が高いが、ユーザープログラムの制御外のものであることを意味します。つまり、変数を参照する場合、プログラムは常に物理アドレス(つまり、マップされた入力FIFO)を確認し、それをキャッシュされた方法で使用しないでください。
Wikiはvolatile
についてすべてを述べています。
そしてLinuxカーネルのドキュメントもvolatile
について優れた表記をしています。
簡単に言えば、特定の変数に対して最適化を行わないようにコンパイラーに指示します。デバイスレジスタにマップされている変数は、デバイスによって間接的に変更されます。この場合は、volatileを使用する必要があります。
私の意見では、あなたはvolatile
からあまり期待してはいけません。説明するために、 Nils Pipenbrinckの高得票数回答 の例を見てください。
彼の例はvolatile
には適していません。 volatile
は、コンパイラが有用で望ましい最適化を行わないようにするためにのみ使用されます。スレッドセーフ、アトミックアクセス、さらにはメモリの順序については関係ありません。
その例では:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
gadget->data = data
のみの前のgadget->command = command
は、コンパイラによってコンパイルされたコードでのみ保証されています。実行時には、プロセッサアーキテクチャに関して、プロセッサは依然としてデータとコマンドの割り当てを並べ替える可能性があります。ハードウェアが誤ったデータを取得する可能性があります(ガジェットがハードウェアI/Oにマップされているとします)。データとコマンドの割り当ての間にメモリバリアが必要です。
揮発性は、コンパイルされたコードの外部から変更することができます(例えば、プログラムは揮発性変数をメモリマップドレジスタにマップすることができます)。 tメモリに書き込まずにレジスタにロードする。これはハードウェアレジスタを扱うときに重要です。
Dennis Ritchieによって設計された言語では、アドレスがとられていない自動オブジェクト以外のオブジェクトへのすべてのアクセスは、あたかもオブジェクトのアドレスを計算してからそのアドレスでストレージを読み書きするように振る舞います。これは言語を非常に強力にしましたが、最適化の機会は非常に限られていました。
特定のオブジェクトが奇妙な方法で変更されないと仮定するようにコンパイラーに依頼する修飾子を追加することは可能かもしれませんが、そのような仮定はCプログラムの大多数のオブジェクトに適切であり、そのような仮定が適切であるであろうすべてのオブジェクトに修飾子を追加することは実用的ではありませんでした。一方、プログラムによっては、そのような仮定が成り立たないようなオブジェクトを使用する必要があります。この問題を解決するために、規格では、volatile
として宣言されていないオブジェクトは、その値が監視されたり変更されたりしない、または合理的なコンパイラの理解から外れることを想定しています。
さまざまなプラットフォームでは、コンパイラの制御外でオブジェクトを監視または変更する方法が異なる場合があるため、それらのプラットフォーム用の高品質コンパイラでは、volatile
セマンティクスの厳密な処理方法が異なるのが適切です。残念ながら、プラットフォーム上での低レベルプログラミングを目的とした高品質コンパイラがそのプラットフォーム上の特定の読み書き操作のありとあらゆる影響を認識できるようにvolatile
を扱うべきであると標準が提案しなかったので、多くのコンパイラは不十分ですバックグラウンドI/Oのようなものを効率的な方法で処理するのを難しくする方法で行いますが、コンパイラの「最適化」によって壊すことはできません。