Cでvolatileが必要なのはなぜですか?


回答:


425

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;
    }

46
個人的には、ハードウェアと通信するときは整数サイズを明示的に指定することをお勧めします(例:int8 / int16 / int32)。でもいい答えです;)
tonylo

22
はい、あなたは固定レジスタサイズで物事を宣言する必要がありますが、ちょっと-それは単なる例です。
Nils Pipenbrinck 2008年

69
同時実行性が保護されていないデータを操作する場合も、スレッドコードで揮発性が必要です。そして、そうするのに有効な時間があります。たとえば、明示的な同時実行性保護を必要とせずに、スレッドセーフな循環メッセージキューを作成できますが、揮発性が必要になります。
Gordon Wrigley、

14
C仕様をもっとよく読んでください。Volatileは、メモリマップされたデバイスI / Oまたは非同期割り込み機能によって影響を受けるメモリでのみ動作を定義しています。スレッド化については何も言われておらず、複数のスレッドが触れたメモリへのアクセスを最適化するコンパイラは準拠しています。
2008年

17
@tolomea:完全に間違っています。悲しい17人はそれを知らない。volatileはメモリフェンスではありません。これは、目に見えない副作用の仮定に基づいて、最適化中にコードの省略回避することにのみ関連しています
v.oddou

187

volatileCでは実際には、変数の値を自動的にキャッシュしないために存在しました。この変数の値をキャッシュしないようコンパイラーに指示します。したがって、指定されたvolatile変数に遭遇するたびに、その値をメインメモリから取得するコードを生成します。このメカニズムが使用されるのは、OSまたは割り込みによっていつでも値を変更できるためです。したがって、使用volatileすることで、毎回新しい値にアクセスできます。


存在し始めましたか?「volatile」は元々C ++から借用されたのではないですか?まあ、私は覚えているようです...
syntaxerror

これは、およそ揮発性のすべてではありません-指定された場合、それはまた、揮発性としていくつかの並べ替えを禁止...
FaceBro

4
@FaceBro:の目的はvolatile、コンパイラーがコードを最適化できるようにすると同時に、そのような最適化なしで達成されるであろうセマンティクスをプログラマーが達成できるようにすることでした。標準の作成者は、ターゲットプラットフォームとアプリケーションフィールドを考慮して、高品質の実装が有用なセマンティクスをサポートすることを期待し、コンパイラの作成者が標準に準拠し、100%ではない最低品質のセマンティクスを提供することを期待していませんでした。ばかげている(標準の作成者が論理的根拠で明確に認識していることに注意してください...
supercat

1
...実装が実際にはどの目的にも適しているほど十分に品質が高くなくても準拠することは可能ですが、それを防ぐ必要はないと考えていました)。
スーパーキャット2018年

1
@syntaxerror CがC ++よりも10年以上古かったときに(最初のリリースと最初の標準の両方で)、C ++からどのように借りることができますか?
phuclv

178

もう1つの用途volatileはシグナルハンドラです。次のようなコードがある場合:

int quit = 0;
while (!quit)
{
    /* very small loop which is completely visible to the compiler */
}

コンパイラーは、ループ本体がquit変数に触れておらず、ループをループに変換しないことに気付くことができwhile (true)ます。andのquitシグナルハンドラーに変数が設定されている場合でも、コンパイラはそれを知る方法がありません。SIGINTSIGTERM

ただし、quit変数が宣言volatileされている場合、他の場所で変更できるため、コンパイラーは毎回変数をロードする必要があります。これはまさにこの状況で必要なものです。


「コンパイラは毎回それをロードするように強制されます。コンパイラが特定の変数を最適化することを決定し、変数を揮発性として宣言しないとき、実行時に特定の変数がメモリ内ではないCPUレジスタにロードされるようなものです。 ?
Amit Singh Tomar

1
@AmitSinghTomarそれはそれが言うことを意味します:コードが値をチェックするたびに、それはリロードされます。そうでない場合、コンパイラーは、変数への参照を取得しない関数は変数を変更できないと見なすことができます。したがって、CesarBが上記のループが設定しないことを意図していたと仮定すると、quitコンパイラーはそれを定数ループに最適化できます。quitイテレーション間で変更する方法がないこと。注意:これは、必ずしも実際のスレッドセーフなプログラミングの代わりになるものではありません。
underscore_d

quitがグローバル変数の場合、コンパイラはwhileループを最適化しません。正しいですか?
Pierre G.

2
@PierreG。いいえ、特に指示がない限り、コンパイラーは常にコードがシングルスレッドであると想定できます。つまり、volatileマーカーが存在しない場合や他のマーカーがない場合は、ループに入ると、グローバル変数であっても、ループの外側では何もその変数を変更しないと見なされます。
CesarB 2016

1
@PierreG。はい、たとえば、コンパイル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して違いを確認してください。
CesarB 2016

60

volatile変数にアクセスしているコード以外の方法で変数が変更される可能性があることをコンパイラーに伝えます。たとえば、I / Oにマップされたメモリ位置である場合があります。このような場合にこれが指定されていない場合は、一部の変数アクセスを最適化できます。たとえば、その内容をレジスターに保持したり、メモリー位置を再度読み取ったりすることができません。


30

Andrei Alexandrescuによるこの記事を参照してください。「volatile-マルチスレッドプログラマーの親友

揮発性のキーワードは、特定の非同期イベントの存在下で、不正なコードをレンダリングする可能性があるコンパイラの最適化を防ぐために考案されました。たとえば、プリミティブ変数をvolatileとして宣言した場合 、コンパイラーはそれをレジスターにキャッシュすることを許可されません-変数が複数のスレッド間で共有された場合に悲惨になる一般的な最適化。したがって、一般的なルールは、複数のスレッド間で共有する必要があるプリミティブ型の変数がある場合、それらの変数を揮発性として宣言することです。。しかし、実際にはこのキーワードを使用してさらに多くのことができます。これを使用して、スレッドセーフではないコードをキャッチし、コンパイル時にそれを行うことができます。この記事では、その方法を示します。ソリューションには、コードの重要なセクションのシリアル化を容易にする単純なスマートポインターが含まれます。

記事は、両方に適用されますCC++

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保証です。


規格は、値が決して使用されない場合、読み取りが「観察可能な動作」と見なされるかどうかを指定していますか?私の印象はそうあるべきだと思いますが、私がそれを別の場所にあると主張したとき、誰かが私に引用を求めて挑戦しました。揮発性変数の読み取りが何らかの影響を与える可能性があると思われるプラットフォームでは、コンパイラーは、指示されたすべての読み取りを正確に1回実行するコードを生成する必要があります。その要件がないと、予測可能な読み取りシーケンスを生成するコードを作成することが困難になります。
スーパーキャット

@supercat:最初の記事によると、「変数にvolatile修飾子を使用すると、コンパイラーはその変数をレジスターにキャッシュしません—各アクセスはその変数の実際のメモリー位置にヒットします。」また、c99標準のセクション6.7.3.6で、「揮発性修飾型のオブジェクトは、実装にとって未知の方法で変更されたり、他の未知の副作用がある可能性があります」と述べています。さらに、揮発性変数はレジスターにキャッシュされない可能性があり、すべての読み取りと書き込みはシーケンスポイントに対して相対的に実行する必要があること、つまり実際に観察可能であることを意味します。
ロバートS.バーンズ

実際、後者の記事では、読み取りは副作用であると明示されています。前者は、読み取りを順序どおりに実行できないことを示していますが、完全に除外される可能性を排除していないようです。
supercat

「コンパイラーはそれをレジスターにキャッシュすることを許可されていません」-ほとんどのRISCアーキテクチャはレジスターマシンであるため、読み取り、変更、書き込みオブジェクトをレジスターにキャッシュする必要があります。volatile原子性は保証されません。
このサイトには正直すぎる

1
@Olaf:レジスタに何かをロードすることは、キャッシングと同じではありません。キャッシングは、ロードまたはストアの数、またはそれらのタイミングに影響します。
スーパーキャット2018年

28

私の簡単な説明は:

一部のシナリオでは、ロジックまたはコードに基づいて、コンパイラーは変化しないと思われる変数の最適化を行います。volatile変数が最適化されているキーワードを防ぎます。

例えば:

bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
    // execute logic for the scenario where the USB isn't connected 
}

上記のコードから、コンパイラーはusb_interface_flag0として定義されていると考える可能性があり、whileループではそれは永久にゼロになります。最適化後、コンパイラはwhile(true)常にそれを処理し、無限ループが発生します。

これらの種類のシナリオを回避するために、フラグを揮発性として宣言します。この値は外部インターフェイスまたはプログラムの他のモジュールによって変更される可能性があることをコンパイラーに伝えています。つまり、最適化しないでください。これがvolatileの使用例です。


19

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
セルゲイジューコフ2014

2
hとは、hhつまりhh操作によって両者の何らかの負のパワーに切り捨てられますx + h - x。この場合、x + hhとはx正確に異なりますhh。また、数式を使用することもできます。これは、x + hx + hhが等しいため、同じ結果になります(ここで重要なのは分母です)。
Alexandre C.

3
これを書くためのより読みやすい方法はないでしょうx1=x+h; d = (f(x1)-f(x))/(x1-x)か?揮発性を使用せず。
セルゲイジューコフ2014

コンパイラーが関数のその2行目を一掃できる参照はありますか?
CoffeeTableEspresso 2018

@CoffeeTableEspresso:いいえ、申し訳ありません。浮動小数点について私が知っているほど、コンパイラーは、明示的-ffast-mathまたは同等の方法で明示的に指示された場合にのみ最適化できると考えています。
Alexandre C.

11

2つの用途があります。これらは、組み込み開発で特に頻繁に使用されます。

  1. コンパイラーは、volatileキーワードで定義された変数を使用する関数を最適化しません

  2. Volatileは、RAM、ROMなどの正確なメモリ位置にアクセスするために使用されます。これは、メモリマップされたデバイスの制御、CPUレジスタへのアクセス、および特定のメモリ位置の検索によく使用されます。

アセンブリリストの例を参照してください。 Re:組み込み開発におけるCの「volatile」キーワードの使用


「コンパイラーは、volatileキーワードで定義された変数を使用する関数を最適化しません」-それは明らかに間違っています。
このサイトには正直すぎる

10

揮発性は、コンパイラーに特定のコードシーケンスを最適化しないように強制する場合にも役立ちます(たとえば、マイクロベンチマークを書き込むため)。


10

揮発性が重要な別のシナリオについて説明します。

I / Oを高速化するためにファイルをメモリマップし、そのファイルがバックグラウンドで変更される可能性があるとします(たとえば、ファイルはローカルハードドライブ上ではなく、別のコンピューターによってネットワーク経由で提供されます)。

不揮発性オブジェクトへのポインタ(ソースコードレベル)を介してメモリマップファイルのデータにアクセスする場合、コンパイラによって生成されたコードは、気付かないうちに同じデータを複数回フェッチする可能性があります。

そのデータが変更された場合、プログラムは2つ以上の異なるバージョンのデータを使用するようになり、不整合な状態になる可能性があります。これは、プログラムの論理的に正しくない動作だけでなく、信頼できないファイルまたは信頼できない場所からのファイルを処理する場合に、プログラムに悪用可能なセキュリティホールをもたらす可能性があります。

セキュリティに関心がある場合は、これは考慮すべき重要なシナリオです。


7

揮発性とは、ストレージがいつでも変更され、変更される可能性が高いことを意味しますが、ユーザープログラムの制御外のものです。つまり、変数を参照する場合、プログラムは常に物理アドレス(つまり、マップされた入力fifo)をチェックし、キャッシュされた方法では使用しないでください。


「RAMの物理アドレス」または「キャッシュのバイパス」を意味する揮発性のコンパイラはありません。
curiousguy


5

私の意見では、からあまり期待してはいけません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 = datagadget->command = commandのみのみコンパイラによってコンパイルされたコードで保証されています。実行時に、プロセッサは、プロセッサアーキテクチャに関して、データとコマンドの割り当てを並べ替える可能性があります。ハードウェアが誤ったデータを取得する可能性があります(ガジェットがハードウェアI / Oにマップされていると想定)。メモリバリアは、データとコマンドの割り当ての間に必要です。


2
volatileは、コンパイラーが通常有用で望ましい最適化を行わないようにするために使用されます。書かれているようvolatileに、理由もなくパフォーマンスが低下しているようです。それで十分かどうかは、プログラマーがコンパイラーよりも知っているシステムの他の側面に依存します。一方、特定のアドレスに書き込む命令がCPUキャッシュをフラッシュすることをプロセッサが保証しているが、コンパイラがCPUが何も知らないレジスタキャッシュ変数をフラッシュする方法を提供していない場合、キャッシュのフラッシュは役に立たないでしょう。
スーパーキャット2017

5

Dennis Ritchieによって設計された言語では、アドレスが取得されなかった自動オブジェクト以外のオブジェクトへのすべてのアクセスは、オブジェクトのアドレスを計算し、そのアドレスでストレージを読み書きしたかのように動作します。これにより言語は非常に強力になりましたが、最適化の機会は大幅に制限されました。

特定のオブジェクトが奇妙な方法で変更されないことをコンパイラに想定させる修飾子を追加することは可能かもしれませんが、そのような想定はCプログラムの大多数のオブジェクトに適切であり、このような仮定が適切であるすべてのオブジェクトに修飾子を追加することは非現実的です。一方、一部のプログラムでは、このような仮定が成り立たないオブジェクトを使用する必要があります。この問題を解決するために、標準では、宣言volatileされていないオブジェクトは、コンパイラの制御の及ばない方法で値が監視または変更されないか、妥当なコンパイラの理解の範囲外であるとコンパイラが想定する可能性があると述べています。

さまざまなプラットフォームでは、コンパイラーの制御外でオブジェクトを監視または変更する方法が異なる可能性があるため、これらのプラットフォームの高品質コンパイラーは、volatileセマンティクスの正確な処理が異なる必要があります。残念ながら、標準では、プラットフォームでの低レベルのプログラミングを目的とした高品質のコンパイラは、そのプラットフォームvolatileでの特定の読み取り/書き込み操作の関連するすべての影響を認識する方法で処理する必要があると提案していなかったため、多くのコンパイラでは不十分です。そのため、効率的な方法でバックグラウンドI / Oなどの処理を困難にするが、コンパイラーの「最適化」によって壊すことはできません。


5

簡単に言えば、特定の変数に対して最適化を行わないようにコンパイラーに指示します。デバイスレジスタにマップされる変数は、デバイスによって間接的に変更されます。この場合、揮発性を使用する必要があります。


1
これまでに述べられていないこの回答の新しいものはありますか?
slfan 2018年

3

揮発性は、コンパイル済みコードの外部から変更できます(たとえば、プログラムは揮発性変数をメモリマップドレジスタにマップする場合があります)。コンパイラーは、揮発性変数を処理するコードに特定の最適化を適用しません-たとえば、 tメモリに書き込まずにレジスタにロードします。これは、ハードウェアレジスタを扱うときに重要です。


0

ここで多くの人が正しく示唆しているように、volatileキーワードの一般的な使用法は、volatile変数の最適化をスキップすることです。

頭に浮かぶ最大の利点は、揮発性について読んだ後に言及する価値があります- ロールバックを防ぐことです場合に変数のlongjmpです。非局所ジャンプ。

これは何を意味するのでしょうか?

これは単に、以前のスタックフレームに戻るために、スタックを巻き戻した後、最後の値が保持されることを意味します。通常、エラーのあるシナリオの場合。

この質問の範囲外なので、setjmp/longjmpここでは詳しく説明しませんが、それについて読む価値はあります。ボラティリティ機能を使用して最後の値を保持する方法。


-2

コンパイラが変数の値を自動的に変更することはできません。volatile変数は動的に使用するためのものです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.