なぜvolatile
C で必要なのですか?何に使うの?それは何をしますか?
なぜvolatile
C で必要なのですか?何に使うの?それは何をしますか?
回答:
Volatileは、揮発性変数に関連することは何も最適化しないようコンパイラーに指示します。
これを使用する一般的な理由は少なくとも3つあり、すべて、可視コードからのアクションなしで変数の値が変化する可能性がある状況に関係しています。変数を使用する別のスレッドが実行されている場合。または、変数の値を変更する可能性のあるシグナルハンドラーがある場合。
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の値を1回だけ読み取ってから、無限ループに入ります。それはあなたが望むものではありません。
これを回避する方法は、ポインタガジェットを揮発性として宣言することです。このようにして、コンパイラーはユーザーが記述したことを強制的に実行します。メモリの割り当てを削除したり、変数をレジスタにキャッシュしたり、割り当ての順序を変更したりすることはできません。
これは正しいバージョンです:
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;
}
volatile
Cでは実際には、変数の値を自動的にキャッシュしないために存在しました。この変数の値をキャッシュしないようコンパイラーに指示します。したがって、指定されたvolatile
変数に遭遇するたびに、その値をメインメモリから取得するコードを生成します。このメカニズムが使用されるのは、OSまたは割り込みによっていつでも値を変更できるためです。したがって、使用volatile
することで、毎回新しい値にアクセスできます。
volatile
、コンパイラーがコードを最適化できるようにすると同時に、そのような最適化なしで達成されるであろうセマンティクスをプログラマーが達成できるようにすることでした。標準の作成者は、ターゲットプラットフォームとアプリケーションフィールドを考慮して、高品質の実装が有用なセマンティクスをサポートすることを期待し、コンパイラの作成者が標準に準拠し、100%ではない最低品質のセマンティクスを提供することを期待していませんでした。ばかげている(標準の作成者が論理的根拠で明確に認識していることに注意してください...
もう1つの用途volatile
はシグナルハンドラです。次のようなコードがある場合:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
コンパイラーは、ループ本体がquit
変数に触れておらず、ループをループに変換しないことに気付くことができwhile (true)
ます。andのquit
シグナルハンドラーに変数が設定されている場合でも、コンパイラはそれを知る方法がありません。SIGINT
SIGTERM
ただし、quit
変数が宣言volatile
されている場合、他の場所で変更できるため、コンパイラーは毎回変数をロードする必要があります。これはまさにこの状況で必要なものです。
quit
コンパイラーはそれを定数ループに最適化できます。quit
イテレーション間で変更する方法がないこと。注意:これは、必ずしも実際のスレッドセーフなプログラミングの代わりになるものではありません。
volatile
マーカーが存在しない場合や他のマーカーがない場合は、ループに入ると、グローバル変数であっても、ループの外側では何もその変数を変更しないと見なされます。
extern int global; void fn(void) { while (global != 0) { } }
しgcc -O3 -S
て結果のアセンブリファイルを確認しmovl global(%rip), %eax
ます。私のマシンでは、そうです。testl %eax, %eax
; je .L1
; .L4: jmp .L4
、つまり、グローバルがゼロでない場合、無限ループ。次に、追加volatile
して違いを確認してください。
volatile
変数にアクセスしているコード以外の方法で変数が変更される可能性があることをコンパイラーに伝えます。たとえば、I / Oにマップされたメモリ位置である場合があります。このような場合にこれが指定されていない場合は、一部の変数アクセスを最適化できます。たとえば、その内容をレジスターに保持したり、メモリー位置を再度読み取ったりすることができません。
Andrei Alexandrescuによるこの記事を参照してください。「volatile-マルチスレッドプログラマーの親友」
揮発性のキーワードは、特定の非同期イベントの存在下で、不正なコードをレンダリングする可能性があるコンパイラの最適化を防ぐために考案されました。たとえば、プリミティブ変数をvolatileとして宣言した場合 、コンパイラーはそれをレジスターにキャッシュすることを許可されません-変数が複数のスレッド間で共有された場合に悲惨になる一般的な最適化。したがって、一般的なルールは、複数のスレッド間で共有する必要があるプリミティブ型の変数がある場合、それらの変数を揮発性として宣言することです。。しかし、実際にはこのキーワードを使用してさらに多くのことができます。これを使用して、スレッドセーフではないコードをキャッチし、コンパイル時にそれを行うことができます。この記事では、その方法を示します。ソリューションには、コードの重要なセクションのシリアル化を容易にする単純なスマートポインターが含まれます。
記事は、両方に適用されますC
とC++
。
Scott MeyersとAndrei Alexandrescuによる記事「C ++ and the Perils of Double-Checked Locking」も参照してください。
そのため、一部のメモリロケーション(ISRによって参照されるメモリマップポートまたはメモリ[割り込みサービスルーチン]など)を処理する場合、一部の最適化を中断する必要があります。volatileは、そのような場所の特別な扱いを指定するために存在します。具体的には、(1)volatile変数の内容が「不安定」(コンパイラーにとって不明な方法で変更される可能性があります)、(2)volatileデータへのすべての書き込みが「観測可能」であるため、 (3)揮発性データに対するすべての操作は、ソースコードに現れる順序で実行されます。最初の2つのルールは、適切な読み取りと書き込みを保証します。最後のものは、入力と出力を混合するI / Oプロトコルの実装を可能にします。これは非公式にCおよびC ++のvolatile保証です。
volatile
原子性は保証されません。
私の簡単な説明は:
一部のシナリオでは、ロジックまたはコードに基づいて、コンパイラーは変化しないと思われる変数の最適化を行います。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ループではそれは永久にゼロになります。最適化後、コンパイラはwhile(true)
常にそれを処理し、無限ループが発生します。
これらの種類のシナリオを回避するために、フラグを揮発性として宣言します。この値は外部インターフェイスまたはプログラムの他のモジュールによって変更される可能性があることをコンパイラーに伝えています。つまり、最適化しないでください。これが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を含むメモリー位置を読み取らせ、最終的な最適化の機会を失います。
h
か、hh
派生式は?ときにhh
計算され、最後の式は差がないと、最初のもののようにそれを使用します。たぶんそうなの(f(x+h) - f(x))/hh
?
h
とは、hh
つまりhh
操作によって両者の何らかの負のパワーに切り捨てられますx + h - x
。この場合、x + hh
とはx
正確に異なりますhh
。また、数式を使用することもできます。これは、x + h
とx + hh
が等しいため、同じ結果になります(ここで重要なのは分母です)。
x1=x+h; d = (f(x1)-f(x))/(x1-x)
か?揮発性を使用せず。
-ffast-math
または同等の方法で明示的に指示された場合にのみ最適化できると考えています。
2つの用途があります。これらは、組み込み開発で特に頻繁に使用されます。
コンパイラーは、volatileキーワードで定義された変数を使用する関数を最適化しません
Volatileは、RAM、ROMなどの正確なメモリ位置にアクセスするために使用されます。これは、メモリマップされたデバイスの制御、CPUレジスタへのアクセス、および特定のメモリ位置の検索によく使用されます。
アセンブリリストの例を参照してください。 Re:組み込み開発におけるCの「volatile」キーワードの使用
揮発性は、コンパイラーに特定のコードシーケンスを最適化しないように強制する場合にも役立ちます(たとえば、マイクロベンチマークを書き込むため)。
揮発性が重要な別のシナリオについて説明します。
I / Oを高速化するためにファイルをメモリマップし、そのファイルがバックグラウンドで変更される可能性があるとします(たとえば、ファイルはローカルハードドライブ上ではなく、別のコンピューターによってネットワーク経由で提供されます)。
不揮発性オブジェクトへのポインタ(ソースコードレベル)を介してメモリマップファイルのデータにアクセスする場合、コンパイラによって生成されたコードは、気付かないうちに同じデータを複数回フェッチする可能性があります。
そのデータが変更された場合、プログラムは2つ以上の異なるバージョンのデータを使用するようになり、不整合な状態になる可能性があります。これは、プログラムの論理的に正しくない動作だけでなく、信頼できないファイルまたは信頼できない場所からのファイルを処理する場合に、プログラムに悪用可能なセキュリティホールをもたらす可能性があります。
セキュリティに関心がある場合は、これは考慮すべき重要なシナリオです。
揮発性とは、ストレージがいつでも変更され、変更される可能性が高いことを意味しますが、ユーザープログラムの制御外のものです。つまり、変数を参照する場合、プログラムは常に物理アドレス(つまり、マップされた入力fifo)をチェックし、キャッシュされた方法では使用しないでください。
ウィキは次のことについてすべて言っていますvolatile
:
また、Linuxカーネルのドキュメントにも、次のような優れた表記があり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にマップされていると想定)。メモリバリアは、データとコマンドの割り当ての間に必要です。
volatile
に、理由もなくパフォーマンスが低下しているようです。それで十分かどうかは、プログラマーがコンパイラーよりも知っているシステムの他の側面に依存します。一方、特定のアドレスに書き込む命令がCPUキャッシュをフラッシュすることをプロセッサが保証しているが、コンパイラがCPUが何も知らないレジスタキャッシュ変数をフラッシュする方法を提供していない場合、キャッシュのフラッシュは役に立たないでしょう。
Dennis Ritchieによって設計された言語では、アドレスが取得されなかった自動オブジェクト以外のオブジェクトへのすべてのアクセスは、オブジェクトのアドレスを計算し、そのアドレスでストレージを読み書きしたかのように動作します。これにより言語は非常に強力になりましたが、最適化の機会は大幅に制限されました。
特定のオブジェクトが奇妙な方法で変更されないことをコンパイラに想定させる修飾子を追加することは可能かもしれませんが、そのような想定はCプログラムの大多数のオブジェクトに適切であり、このような仮定が適切であるすべてのオブジェクトに修飾子を追加することは非現実的です。一方、一部のプログラムでは、このような仮定が成り立たないオブジェクトを使用する必要があります。この問題を解決するために、標準では、宣言volatile
されていないオブジェクトは、コンパイラの制御の及ばない方法で値が監視または変更されないか、妥当なコンパイラの理解の範囲外であるとコンパイラが想定する可能性があると述べています。
さまざまなプラットフォームでは、コンパイラーの制御外でオブジェクトを監視または変更する方法が異なる可能性があるため、これらのプラットフォームの高品質コンパイラーは、volatile
セマンティクスの正確な処理が異なる必要があります。残念ながら、標準では、プラットフォームでの低レベルのプログラミングを目的とした高品質のコンパイラは、そのプラットフォームvolatile
での特定の読み取り/書き込み操作の関連するすべての影響を認識する方法で処理する必要があると提案していなかったため、多くのコンパイラでは不十分です。そのため、効率的な方法でバックグラウンドI / Oなどの処理を困難にするが、コンパイラーの「最適化」によって壊すことはできません。
簡単に言えば、特定の変数に対して最適化を行わないようにコンパイラーに指示します。デバイスレジスタにマップされる変数は、デバイスによって間接的に変更されます。この場合、揮発性を使用する必要があります。
揮発性は、コンパイル済みコードの外部から変更できます(たとえば、プログラムは揮発性変数をメモリマップドレジスタにマップする場合があります)。コンパイラーは、揮発性変数を処理するコードに特定の最適化を適用しません-たとえば、 tメモリに書き込まずにレジスタにロードします。これは、ハードウェアレジスタを扱うときに重要です。
ここで多くの人が正しく示唆しているように、volatileキーワードの一般的な使用法は、volatile変数の最適化をスキップすることです。
頭に浮かぶ最大の利点は、揮発性について読んだ後に言及する価値があります- ロールバックを防ぐことです場合に変数のlongjmp
です。非局所ジャンプ。
これは何を意味するのでしょうか?
これは単に、以前のスタックフレームに戻るために、スタックを巻き戻した後、最後の値が保持されることを意味します。通常、エラーのあるシナリオの場合。
この質問の範囲外なので、setjmp/longjmp
ここでは詳しく説明しませんが、それについて読む価値はあります。ボラティリティ機能を使用して最後の値を保持する方法。