C ++のvolatileキーワードはメモリフェンスを導入しますか?


85

volatile値が変更される可能性があることをコンパイラに通知することを理解していますが、この機能を実現するために、コンパイラはそれを機能させるためにメモリフェンスを導入する必要がありますか?

私の理解では、揮発性オブジェクトに対する一連の操作は並べ替えることができず、保持する必要があります。これは、いくつかのメモリフェンスが必要であり、これを回避する方法が実際にはないことを意味しているようです。私はこれを言うのは正しいですか?


この関連する質問で興味深い議論があります

ジョナサンウェイクリーは書いています

...個別の揮発性変数へのアクセスは、それらが別々の完全な式で発生する限り、コンパイラーによって並べ替えることはできません...揮発性はスレッドセーフには役に立たないが、彼が与える理由ではありません。これは、コンパイラが揮発性オブジェクトへのアクセスを並べ替える可能性があるためではなく、CPUがそれらを並べ替える可能性があるためです。アトミック操作とメモリバリアにより、コンパイラとCPUの並べ替えが妨げられます

これにデビッド・シュワルツ氏は返信コメントで

... C ++標準の観点からは、コンパイラが何かを実行することと、コンパイラがハードウェアに何かを実行させる命令を発行することとの間に違いはありません。CPUが揮発性物質へのアクセスを並べ替える可能性がある場合、標準ではそれらの順序を保持する必要はありません。..。

... C ++標準では、並べ替えの内容を区別していません。そして、CPUが観察可能な影響なしにそれらを並べ替えることができると主張することはできないので、それは問題ありません-C ++標準はそれらの順序を観察可能として定義しています。コンパイラーは、プラットフォームに標準が要求することを実行させるコードを生成する場合、プラットフォーム上のC ++標準に準拠しています。標準が揮発性物質へのアクセスを並べ替えないことを要求している場合、プラットフォームは揮発性物質を並べ替えることに準拠していません。..。

私のポイントは、C ++標準がコンパイラーによる個別の揮発性物質へのアクセスの並べ替えを禁止している場合、そのようなアクセスの順序はプログラムの観察可能な動作の一部であるという理論に基づいて、CPUが実行することを禁止するコードを発行することもコンパイラーに要求するということです。そう。この規格は、コンパイラーが行うことと、コンパイラーの生成コードがCPUに行うことを区別していません。

どちらが2つの質問をもたらします:どちらかが「正しい」ですか?実際の実装は実際に何をしますか?


9
これは主に、コンパイラがその変数をレジスタに保持してはならないことを意味します。すべての割り当てとソースコードの読み取りは、バイナリコードのメモリアクセスに対応している必要があります。
Basile Starynkevitch 2014年


1
重要なのは、値が内部レジスタに格納されている場合、メモリフェンスは効果がないということだと思います。同時に、他の保護対策を講じる必要があると思います。
ガリック2014年

私の知る限り、volatileは、ハードウェアによって変更できる変数に使用されます(多くの場合、マイクロコントローラーで使用されます)。これは単に、変数の読み取りを別の順序で実行したり、最適化したりすることができないことを意味します。これはCですが、++でも同じである必要があります。
マスト

1
@Mastvolatile変数の読み取りがCPUキャッシュによって最適化されないようにするコンパイラーはまだ見たことがありません。これらのコンパイラはすべて非準拠であるか、標準はあなたがそれが何を意味すると思うかを意味しません。(標準は、コンパイラーが実行することとコンパイラーがCPUに実行させることを区別しません。実行時に標準に準拠するコードを出力するのはコンパイラーの仕事です。)
David Schwartz

回答:


58

何をするのかを説明するのでvolatileはなく、いつ使用すべきかを説明させてくださいvolatile

  • シグナルハンドラー内の場合。volatile変数への書き込みは、標準でシグナルハンドラー内から実行できる唯一のことであるためです。C ++ 11以降std::atomic、その目的に使用できますが、アトミックにロックがない場合に限ります。
  • setjmp Intelに従って扱う場合。
  • ハードウェアを直接処理し、コンパイラーが読み取りまたは書き込みを最適化しないようにする場合。

例えば:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

volatile指定子がない場合、コンパイラーはループを完全に最適化することができます。volatile指定子は、それは、その後の2が同じ値を返す読み込むことを前提としないかもしれないことをコンパイラに指示します。

これvolatileはスレッドとは何の関係もないことに注意してください。上記の例*fooは、取得操作が含まれていないため、別のスレッドへの書き込みがあった場合は機能しません。

他のすべての場合、volatileC ++ 11より前のコンパイラおよびコンパイラ拡張(/volatile:msX86 / I64でデフォルトで有効になっているmsvcのスイッチなど)を扱う場合を除いて、の使用は移植性がないと見なされ、コードレビューに合格しないようにする必要があります。


5
これは、「2回の後続の読み取りが同じ値を返すと想定しない場合がある」よりも厳密です。一度だけ読み取ったり、値を破棄したりした場合でも、読み取りを行う必要があります。
philipxy 2014年

1
シグナルハンドラーでの使用はsetjmp、標準が行う2つの保証です。一方、少なくとも最初は、メモリマップドIOをサポートすることを目的としていました。一部のプロセッサでは、フェンスまたはメンバーが必要になる場合があります。
James Kanze 2014年

@philipxy「読み取り」の意味を誰も知らない場合を除きます。たとえば、メモリからの実際の読み取りを実行する必要があるとは誰も信じていません。私が知っているコンパイラは、volatileアクセス時にCPUキャッシュをバイパスしようとしません。
David Schwartz

@JamesKanze:そうではありません。シグナルハンドラーに関して、標準では、シグナル処理中は揮発性のstd :: sig_atomic_tとロックフリーのアトミックオブジェクトのみが定義された値を持つとされています。しかし、揮発性オブジェクトへのアクセスは観察可能な副作用であるとも述べています。
philipxy 2014年

1
@DavidSchwartz一部のコンパイラとアーキテクチャのペアは、標準で指定されたアクセスシーケンスを実際の効果にマップし、作業プログラムは揮発性物質にアクセスしてそれらの効果を取得します。一部のそのようなペアにマッピングがない、または些細な役に立たないマッピングがあるという事実は、実装の品質に関連していますが、目前のポイントには関連していません。
philipxy 2014年

25

C ++のvolatileキーワードはメモリフェンスを導入しますか?

仕様に準拠したC ++コンパイラは、メモリフェンスを導入する必要はありません。あなたの特定のコンパイラはかもしれません; コンパイラの作成者に質問を送信してください。

C ++の「volatile」の機能は、スレッド化とは何の関係もありません。「volatile」の目的は、コンパイラの最適化を無効にして、外因性の条件のために変化しているレジスタからの読み取りが最適化されないようにすることです。別のCPUの別のスレッドによって書き込まれているメモリアドレスは、外因性の条件のために変更されているレジスタですか?いいえ。繰り返しになりますが、一部のコンパイラ作成者が、異なるCPU上の異なるスレッドによって書き込まれるメモリアドレスを、外因性の条件によってレジスタが変更されているかのように扱うことを選択した場合、それが彼らのビジネスです。そうする必要はありません。また、たとえば、メモリフェンスが導入されたとしても、すべてのスレッドが一貫性のあるスレッドを認識できるようにする必要はありません。 揮発性の読み取りと書き込みの順序。

実際、volatileはC / C ++でのスレッド化にはほとんど役に立ちません。ベストプラクティスはそれを避けることです。

さらに、メモリフェンスは、特定のプロセッサアーキテクチャの実装の詳細です。volatileマルチスレッド用に明示的設計されているC#では、プログラムがそもそもフェンスを持たないアーキテクチャで実行されている可能性があるため、仕様ではハーフフェンスが導入されるとは述べられていません。むしろ、この仕様では、コンパイラ、ランタイム、およびCPUがどの最適化を回避するかについて、特定の(非常に弱い)保証を行い、いくつかの副作用の順序付け方法に特定の(非常に弱い)制約を課しています。実際には、これらの最適化はハーフフェンスを使用することで排除されますが、これは実装の詳細であり、将来変更される可能性があります。

マルチスレッドに関連する任意の言語でのvolatileのセマンティクスに関心があるという事実は、スレッド間でメモリを共有することを考えていることを示しています。単にそれをしないことを検討してください。それはあなたのプログラムを理解するのをはるかに難しくし、微妙で再現不可能なバグを含む可能性がはるかに高くなります。


19
「volatileはC / C ++ではほとんど役に立たない。」どういたしまして!非常にユーザーモードのデスクトップ中心の世界観があります...しかし、ほとんどのCおよびC ++コードは、メモリマップドI / Oにvolatileが非常に必要な組み込みシステムで実行されます。
Ben Voigt

12
また、揮発性アクセスが保持される理由は、外因性の条件によってメモリの場所が変わる可能性があるからではありません。アクセス自体がさらなるアクションを引き起こす可能性があります。たとえば、読み取りがFIFOを進めたり、割り込みフラグをクリアしたりすることは非常に一般的です。
Ben Voigt 2014年

3
@BenVoigt:スレッドの問題に効果的に対処するのに役に立たないのは私の意図した意味でした。
Eric Lippert 2014年

4
@DavidSchwartz標準では、メモリマップドIOがどのように機能するかを明らかに保証できません。しかし、メモリマップドIOがvolatileC標準に導入された理由です。それでも、標準では「アクセス」で実際に何が起こるかなどを指定できないため、「揮発性修飾型を持つオブジェクトへのアクセスを構成するものは実装定義です」と記載されています。今日の非常に多くの実装では、アクセスの有用な定義が提供されていません。これは、たとえそれが文字に準拠していても、IMHOが標準の精神に違反しています。
James Kanze 2014年

8
その編集は確かな改善ですが、あなたの説明はまだ「記憶が外因的に変更されるかもしれない」に焦点を合わせすぎています。 volatileセマンティクスはそれよりも強力であり、コンパイラは要求されたすべてのアクセス(1.9 / 8、1.9 / 12)を生成する必要があり、外因性の変更が最終的に検出されることを単に保証するだけではありません(1.10 / 27)。メモリマップドI / Oの世界では、メモリ読み取りには、プロパティゲッターのように任意のロジックを関連付けることができます。あなたが述べたルールに従ってプロパティゲッターへの呼び出しを最適化することはなくvolatile、標準ではそれを許可していません。
Ben Voigt 2014年

13

Davidが見落としているのは、C ++標準では、特定の状況でのみ相互作用する複数のスレッドの動作が指定されており、それ以外はすべて未定義の動作になるという事実です。アトミック変数を使用しない場合、少なくとも1回の書き込みを伴う競合状態は未定義です。

その結果、CPUは同期が欠落しているために未定義の動作を示すプログラムの違いにのみ気付くため、コンパイラは同期命令を完全に放棄する権利があります。


5
うまく説明してくれてありがとう。この規格は、プログラムに未定義の動作がない限り、揮発性物質へのアクセスのシーケンスを観察可能なものとしてのみ定義しています
Jonathan Wakely 2014年

4
プログラムにデータ競合がある場合、標準はプログラムの観察可能な動作に要件を設けていません。コンパイラーは、明示的なバリアーまたはアトミック操作のいずれかを使用して、プログラムに存在するデータ競合を防ぐために、揮発性アクセスにバリアーを追加することは期待されていません。
Jonathan Wakely 2014年

なぜ私がそれを見落としていると思いますか?私の議論のどの部分が無効になると思いますか?私は、コンパイラが同期を完全に放棄する権利があることに100%同意します。
David Schwartz

2
これは単に間違っているか、少なくとも本質を無視しています。 volatileスレッドとは何の関係もありません。本来の目的は、メモリマップドIOをサポートすることでした。また、少なくとも一部のプロセッサでは、メモリマップドIOをサポートするにはフェンスが必要になります。(コンパイラーはこれを行いませんが、それは別の問題です。)
James Kanze 2014年

@JamesKanzevolatileはスレッドと多くの関係volatileがあります。コンパイラがアクセスできることを知らなくてもアクセスできるメモリを扱い、特定のCPU上のスレッド間で共有データを実際に使用する多くのことをカバーします。
curiousguy

12

まず第一に、C ++標準は、非アトミックな読み取り/書き込みを適切に順序付けるために必要なメモリバリアを保証していません。揮発性変数は、MMIO、シグナル処理などでの使用に推奨されます。ほとんどの実装では、揮発性はマルチスレッドには役立ちません。また、一般的には推奨されません。

揮発性アクセスの実装に関しては、これがコンパイラの選択です。

gccの動作について説明しているこの記事では、揮発性オブジェクトをメモリバリアとして使用して、揮発性メモリへの一連の書き込みを順序付けることはできないことを示しています。

iccの動作に関して、このソースは、volatileがメモリアクセスの順序を保証しないことも示していることがわかりました。

MicrosoftVS2013コンパイラの動作は異なります。このドキュメントでは、volatileがRelease / Acquireセマンティクスを適用し、マルチスレッドアプリケーションのロック/リリースでvolatileオブジェクトを使用できるようにする方法について説明します。

考慮する必要があるもう1つの側面は、同じコンパイラーの動作異なる可能性があることです。対象のハードウェアアーキテクチャに応じて揮発性になります。MSVS 2013コンパイラに関するこの投稿では、ARMプラットフォーム用のvolatileを使用したコンパイルの詳細について明確に説明しています。

だから私の答え:

C ++のvolatileキーワードはメモリフェンスを導入しますか?

次のようになります。おそらく、保証の限りではありませんが、いくつかのコンパイラはそれを行う可能性があるわけではありません。あなたはそれがするという事実に頼るべきではありません。


2
最適化を妨げるのではなく、コンパイラが特定の制約を超えてロードとストアを変更するのを防ぐだけです。
ディートリッヒエップ2014年

あなたが何を言っているのかはっきりしていません。一部の不特定のコンパイラーでvolatile、コンパイラーがロード/ストアを並べ替えることができない場合があると言っていますか?それとも、C ++標準ではそうする必要があると言っていますか?そして後者の場合、元の質問で引用された反対の私の議論に答えることができますか?
David Schwartz

@DavidSchwartzこの標準は、volatile左辺値を介したアクセスの(任意のソースからの)並べ替えを防止します。ただし、「アクセス」の定義は実装に任されているため、実装が気にしない場合、これはあまり役に立ちません。
James Kanze 2014年

私は、MSCのコンパイラの一部のバージョンは、のためにフェンスのセマンティクスを実装したのだと思いvolatileますが、ビジュアル・スタジオ2012年にコンパイラから生成されたコードには柵がありません
ジェームズ・観世

@JamesKanzeこれは基本的に、の唯一の移植可能な動作がvolatile標準によって具体的に列挙されたものであることを意味します。(setjmp、信号など。)
David Schwartz

7

私の知る限り、コンパイラはItaniumアーキテクチャにメモリフェンスを挿入するだけです。

このvolatileキーワードは、シグナルハンドラやメモリマップドレジスタなどの非同期変更に最適です。通常、マルチスレッドプログラミングに使用するのは間違ったツールです。


1
ある種。'コンパイラ'(msvc)は、ARM以外のアーキテクチャが対象であり、/ volatile:msスイッチが使用されている場合(デフォルト)にメモリフェンスを挿入します。msdn.microsoft.com/en-us/library/12a04hfd.aspxを参照してください。私の知る限り、他のコンパイラは揮発性変数にフェンスを挿入しません。ハードウェア、シグナルハンドラー、またはc ++ 11に準拠していないコンパイラーを直接処理しない限り、volatileの使用は避けてください。
ステファン

@Stefan No.volatileは、ハードウェアを扱ったことのない多くの用途に非常に役立ちます。実装でC / C ++コードに厳密に従うCPUコードを生成する場合は、必ずを使用してくださいvolatile
curiousguy 2018年

7

それはどのコンパイラ「コンパイラ」であるかによります。Visual C ++は、2005年以降、必要です。ただし、標準では必要ないため、他の一部のコンパイラでは必要ありません。


VC ++ 2012はフェンスを挿入していないようです:int volatile i; int main() { return i; }正確に2つの命令でメインを生成します:mov eax, i; ret 0;
James Kanze 2014年

@JamesKanze:正確にはどのバージョンですか?また、デフォルト以外のコンパイルオプションを使用していますか?私はドキュメント(最初に影響を受けたバージョン)(最新バージョン)に依存しています。これらのドキュメントには、取得と解放のセマンティクスが確実に記載されています。
Ben Voigt 2014年

cl /helpバージョン18.00.21005.1と言います。それが入っているディレクトリはC:\Program Files (x86)\Microsoft Visual Studio 12.0\VCです。コマンドウィンドウのヘッダーにはVS2013と表示されてい/c /O2 /Faます。バージョンに関しては...使用したオプションは。だけでした。(がないと/O2、ローカルスタックフレームも設定されます。ただし、フェンス命令はまだありません。)
James Kanze 2014年

@JamesKanze:アーキテクチャにもっと興味がありました。たとえば、「Microsoft(R)C / C ++ Optimizing Compiler Version 18.00.30723 for x64」x86とx64は、最初からメモリモデルでかなり強力なキャッシュコヒーレンシが保証されているため、おそらくフェンスはありません。 ?
Ben Voigt 2014年

多分。よくわかりません。私がでこれを行ったという事実はmain、コンパイラがプログラム全体を見ることができ、他のスレッドがないこと、または少なくとも私の前に変数への他のアクセスがないことを知ることができる(したがってキャッシュの問題がない可能性がある)ことがこれに影響を与える可能性があります同様に、しかしどういうわけか、私はそれを疑う。
James Kanze 2014年

5

これは主にメモリからのものであり、スレッドなしのC ++ 11より前のものに基づいています。しかし、委員会でのスレッド化に関する議論に参加したことで、volatileスレッド間の同期に使用できる委員会の意図はなかったと言えます。マイクロソフトはそれを提案しましたが、提案は実行されませんでした。

の重要な仕様はvolatile、揮発性物質へのアクセスがIOと同様に「観察可能な動作」を表すことです。コンパイラが特定のIOを並べ替えたり削除したりできないのと同じように、揮発性オブジェクトへのアクセス(より正確には、揮発性修飾型を使用した左辺値式を介したアクセス)を並べ替えたり削除したりすることはできません。volatileの本来の目的は、実際、メモリマップドIOをサポートすることでした。ただし、これに関する「問題」は、「揮発性アクセス」を構成するのは実装によって定義されることです。そして、多くのコンパイラは、定義が「メモリの読み取りまたは書き込みを行う命令が実行された」かのように実装します。実装で指定されている場合、これは無用な定義ではありますが、合法です。(コンパイラの実際の仕様はまだ見つかりません。

ハードウェアがアドレスをメモリマップドIOとして認識し、並べ替えなどを禁止しない限り、メモリマップドIOにvolatileを使用することさえできないため、間違いなく(そしてそれは私が受け入れる議論です)、これは標準の意図に違反します。少なくともSparcまたはIntelアーキテクチャでは。それでもなお、私が見たどのコミラー(Sun CC、g ++、MSC)も、フェンスやメンバーの指示を出力しません。(Microsoftがルールの拡張を提案した頃 volatile、一部のコンパイラはその提案を実装し、揮発性アクセスのフェンス命令を発行したと思います。最近のコンパイラが何をするかは確認していませんが、依存していても驚かないでしょう。いくつかのコンパイラオプションで。私がチェックしたバージョン(VS6.0だったと思います)はフェンスを放出しませんでした。)


コンパイラが揮発性オブジェクトへのアクセスを並べ替えたり削除したりできないと言うのはなぜですか?確かに、アクセスが観察可能な動作である場合は、CPU、書き込みポストバッファ、メモリコントローラ、およびその他すべてがそれらを並べ替えないようにすることもまったく同じように重要です。
David Schwartz

@DavidSchwartzそれは標準が言っていることだからです。確かに、実用的な観点から、私が検証したコンパイラーはまったく役に立たないが、標準のイタチは、適合性を主張できるように(または実際に文書化されている場合は可能である)、これを十分に表現している。
James Kanze 2014年

1
@DavidSchwartz:ペリフェラルへの排他的(またはミューテックス)メモリマップドI / Oの場合、volatileセマンティクスは完全に適切です。通常、このような周辺機器は、メモリ領域をキャッシュ不可として報告します。これは、ハードウェアレベルでの並べ替えに役立ちます。
Ben Voigt 2014年

@BenVoigtどういうわけか、それについて疑問に思いました。プロセッサが、処理しているアドレスがメモリマップドIOであることをどういうわけか「知っている」という考えです。私の知る限り、Sparcはこれをサポートしていないため、Sparc上のSunCCとg ++はメモリマップドIOに使用できなくなります。(これを調べたとき、私は主にSparcに興味がありました。)
James

@JamesKanze:少し検索したところ、Sparcには、キャッシュできないメモリの「代替ビュー」専用のアドレス範囲があるようです。揮発性のアクセスポイントASI_REAL_IOがアドレス空間の一部にある限り、大丈夫だと思います。(アルテラNIOSも同様の手法を使用しており、アドレスの上位ビットがMMUバイパスを制御しています。他にもあると確信しています)
Ben Voigt 2014年

5

する必要はありません。Volatileは同期プリミティブではありません。最適化を無効にするだけです。つまり、抽象マシンで規定されているのと同じ順序で、スレッド内で予測可能な読み取りと書き込みのシーケンスを取得します。ただし、異なるスレッドでの読み取りと書き込みには、そもそも順序がありません。順序を維持する、または維持しないと言っても意味がありません。アド間の順序は同期プリミティブによって確立でき、それらなしでUBを取得します。

メモリバリアに関する少しの説明。一般的なCPUには、いくつかのレベルのメモリアクセスがあります。メモリパイプライン、いくつかのレベルのキャッシュ、次にRAMなどがあります。

メンバー命令はパイプラインをフラッシュします。読み取りと書き込みの実行順序は変更されません。特定の瞬間に未処理のものが強制的に実行されるだけです。マルチスレッドプログラムには便利ですが、それ以外の場合はあまり役に立ちません。

キャッシュは通常、CPU間で自動的にコヒーレントです。キャッシュがRAMと同期していることを確認したい場合は、キャッシュフラッシュが必要です。メンバーとは大きく異なります。


1
つまり、C ++標準では、volatileコンパイラの最適化を無効にするだけだと言っているのですか?それは意味がありません。コンパイラーが実行できる最適化は、少なくとも原則として、CPUによって同様に実行できます。したがって、標準がコンパイラの最適化を無効にするだけだと言った場合、それは、ポータブルコードで信頼できる動作をまったく提供しないことを意味します。しかし、ポータブルコードはsetjmp、シグナルに関する動作に依存する可能性があるため、これは明らかに真実ではありません。
デビッドシュワルツ

1
@DavidSchwartzいいえ、規格にはそのようなことはありません。最適化を無効にすることは、標準を実装するために一般的に行われていることです。この標準では、観察可能な動作が抽象マシンで要求されるのと同じ順序で発生することが要求されています。抽象マシンが順序を必要としない場合、実装は順序を自由に使用できるか、順序をまったく使用しません。追加の同期が適用されない限り、異なるスレッドの揮発性変数へのアクセスは順序付けられません。
N。「代名詞」m。

1
@DavidSchwartz不正確な表現をお詫びします。この規格では、最適化を無効にする必要はありません。最適化の概念はまったくありません。むしろ、監視可能な読み取りと書き込みのシーケンスが標準に準拠するように、コンパイラーが特定の最適化を無効にすることを実際に要求する動作を指定します。
N。「代名詞」m。

1
それを必要としないことを除いて、標準は実装が「観察可能な読み取りと書き込みのシーケンス」を好きなように定義することを許可しているからです。実装が、最適化を無効にする必要があるように監視可能なシーケンスを定義することを選択した場合、それらはそうします。そうでない場合は、そうではありません。実装がそれを提供することを選択した場合にのみ、予測可能な読み取りと書き込みのシーケンスを取得します。
デビッドシュワルツ

1
いいえ、実装では、単一のアクセスを構成するものを定義する必要があります。このようなアクセスのシーケンスは、抽象マシンによって規定されています。実装は順序を保持する必要があります。この規格では、非規範的な部分ではありますが、「揮発性はオブジェクトを含む積極的な最適化を回避するための実装へのヒントである」と明示的に述べていますが、その意図は明確です。
N。「代名詞」m。

4

コンパイラは、その特定のプラットフォームでの標準作業(、シグナルハンドラなど)で指定されたvolatile用途を使用するために必要な場合にのみ、アクセスの周囲にメモリフェンスを導入する必要があります。volatilesetjmp

一部のコンパイラはvolatile、これらのプラットフォームでより強力または便利にするために、C ++標準で必要とされるものをはるかに超えていることに注意してください。ポータブルコードはvolatile、C ++標準で指定されている以上のことを行うことに依存すべきではありません。


2

私は常に割り込みサービスルーチンでvolatileを使用します。たとえば、ISR(多くの場合アセンブリコード)は一部のメモリ位置を変更し、割り込みコンテキストの外部で実行される上位レベルのコードはvolatileへのポインタを介してメモリ位置にアクセスします。

これは、RAMとメモリマップドIOに対して行います。

ここでの議論に基づくと、これはまだvolatileの有効な使用法であるように見えますが、複数のスレッドやCPUとは何の関係もありません。マイクロコントローラーのコンパイラーが他のアクセスがないことを「知っている」場合(たとえば、すべてがオンチップで、キャッシュがなく、コアが1つしかない)、メモリーフェンスはまったく暗示されていない、コンパイラーだと思います。特定の最適化を防ぐ必要があります。

オブジェクトコードを実行する「システム」にさらに多くのものを積み上げると、ほとんどすべての賭けがオフになります。少なくとも、それが私がこの議論を読んだ方法です。コンパイラはどのようにしてすべてのベースをカバーできるでしょうか?


0

揮発性と命令の並べ替えに関する混乱は、CPUが行う並べ替えの2つの概念に起因すると思います。

  1. アウトオブオーダー実行。
  2. 他のCPUから見たメモリの読み取り/書き込みのシーケンス(各CPUが異なるシーケンスを見る可能性があるという意味での並べ替え)。

揮発性は、シングルスレッド実行(これには割り込みを含む)を想定してコンパイラがコードを生成する方法に影響します。これは、メモリバリア命令については何も意味しませんが、コンパイラがメモリアクセスに関連する特定の種類の最適化を実行することを妨げます。
典型的な例は、レジスタにキャッシュされた値を使用する代わりに、メモリから値を再フェッチすることです。

アウトオブオーダー実行

CPUは、最終結果が元のコードで発生した可能性がある場合に限り、命令を順不同/投機的に実行できます。コンパイラーはすべての状況で正しい変換しか実行できないため、CPUはコンパイラーで許可されていない変換を実行できます。対照的に、CPUはこれらの最適化の有効性をチェックし、それらが正しくないことが判明した場合はそれらを取り消すことができます。

他のCPUから見たメモリの読み取り/書き込みのシーケンス

一連の命令の最終結果である有効な順序は、コンパイラーによって生成されたコードのセマンティクスと一致する必要があります。ただし、CPUによって選択される実際の実行順序は異なる場合があります。他のCPUに見られるような有効な順序(すべてのCPUは異なるビューを持つことができます)は、メモリバリアによって制約される可能性があります。
メモリバリアがCPUのアウトオブオーダー実行をどの程度妨げる可能性があるかわからないため、効果的な順序と実際の順序がどの程度異なるかはわかりません。

出典:


0

私が最新のOpenGLで動作する3Dグラフィックスとゲームエンジン開発のためのオンラインダウンロード可能なビデオチュートリアルを行っている間。私たちはvolatileクラスの1つで使用しました。チュートリアルのウェブサイトはここにあり、volatileキーワードを使用したShader Engineビデオはシリーズビデオ98にあります。これらの作品は私自身のものではありませんが、認定されてMarek A. Krzeminski, MAScおり、これはビデオダウンロードページからの抜粋です。

「ゲームを複数のスレッドで実行できるようになったので、スレッド間でデータを適切に同期することが重要です。このビデオでは、揮発性変数が適切に同期されるように、揮発性ロッククラスを作成する方法を示します...」

また、彼のWebサイトに登録していて、このビデオ内の彼のビデオにアクセスできる場合、彼はプログラミングでの使用に関するこの記事を参照Volatileしていmultithreadingます。

上記のリンクからの記事は次のとおりです:http//www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

volatile:マルチスレッドプログラマーの親友

アンドレイ・アレキサンドレス、2001年2月1日

volatileキーワードは、特定の非同期イベントが存在する場合にコードが正しくなくなる可能性のあるコンパイラーの最適化を防ぐために考案されました。

私はあなたの気分を台無しにしたくありませんが、このコラムはマルチスレッドプログラミングの恐ろしいトピックに対処します。Genericの前回の記事で述べたように、例外安全プログラミングが難しい場合は、マルチスレッドプログラミングと比較して子供の遊びです。

複数のスレッドを使用するプログラムは、一般に、作成、正しいことの証明、デバッグ、保守、および飼いならしが難しいことで有名です。誤ったマルチスレッドプログラムは、グリッチなしで何年も実行される可能性がありますが、いくつかの重要なタイミング条件が満たされたために予期せず実行されます。

言うまでもなく、マルチスレッドコードを作成するプログラマーは、彼女が得ることができるすべての助けを必要としています。このコラムでは、マルチスレッドプログラムの一般的な問題の原因である競合状態に焦点を当て、それらを回避する方法に関する洞察とツールを提供します。驚くべきことに、コンパイラーはそれを支援するために一生懸命働きます。

ほんの少しのキーワード

C標準とC ++標準はどちらもスレッドに関しては目立って沈黙していますが、volatileキーワードの形で、マルチスレッドに少し譲歩します。

よく知られている対応するconstと同様に、volatileは型修飾子です。これは、さまざまなスレッドでアクセスおよび変更される変数と組み合わせて使用​​することを目的としています。基本的に、揮発性がないと、マルチスレッドプログラムの作成が不可能になるか、コンパイラが膨大な最適化の機会を浪費します。説明が整いました。

次のコードについて考えてみます。

class Gadget {
public:
    void Wait() {
        while (!flag_) {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup() {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};

上記のGadget :: Waitの目的は、flag_メンバー変数を毎秒チェックし、その変数が別のスレッドによってtrueに設定されたときに戻ることです。少なくともそれはプログラマーが意図したことですが、残念ながら、Waitは正しくありません。

Suppose the compiler figures out that Sleep(1000) is a call into an external library that cannot possibly modify the member variable flag_. Then the compiler concludes that it can cache flag_ in a register and use that register instead of accessing the slower on-board memory. This is an excellent optimization for single-threaded code, but in this case, it harms correctness: after you call Wait for some Gadget object, although another thread calls Wakeup, Wait will loop forever. This is because the change of flag_ will not be reflected in the register that caches flag_. The optimization is too ... optimistic.

レジスターに変数をキャッシュすることは、ほとんどの場合に適用される非常に価値のある最適化であるため、それを無駄にするのは残念です。CおよびC ++を使用すると、このようなキャッシュを明示的に無効にすることができます。変数にvolatile修飾子を使用すると、コンパイラはその変数をレジスタにキャッシュしません。アクセスするたびに、その変数の実際のメモリ位置に到達します。したがって、ガジェットの待機/ウェイクアップコンボを機能させるために必要なのは、flag_を適切に修飾することだけです。

class Gadget {
public:
    ... as above ...
private:
    volatile bool flag_;
};

volatileの理論的根拠と使用法のほとんどの説明はここで止まり、複数のスレッドで使用するプリミティブ型をvolatile修飾することをお勧めします。ただし、volatileは、C ++のすばらしい型システムの一部であるため、さらに多くのことができます。

ユーザー定義タイプでのvolatileの使用

プリミティブ型だけでなく、ユーザー定義型も揮発性修飾できます。その場合、volatileはconstと同様の方法で型を変更します。(constとvolatileを同じタイプに同時に適用することもできます。)

constとは異なり、volatileはプリミティブ型とユーザー定義型を区別します。つまり、クラスとは異なり、プリミティブ型は、揮発性修飾されている場合でも、すべての操作(加算、乗算、割り当てなど)をサポートします。たとえば、非揮発性intを揮発性intに割り当てることはできますが、非揮発性オブジェクトを揮発性オブジェクトに割り当てることはできません。

例のユーザー定義型でvolatileがどのように機能するかを説明しましょう。

class Gadget {
public:
    void Foo() volatile;
    void Bar();
    ...
private:
    String name_;
    int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;

volatileがオブジェクトでそれほど役に立たないと思う場合は、いくつかの驚きに備えてください。

volatileGadget.Foo(); // ok, volatile fun called for
                  // volatile object
regularGadget.Foo();  // ok, volatile fun called for
                  // non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
                  // volatile object!

修飾されていない型からその揮発性の対応する型への変換は簡単です。ただし、constの場合と同様に、揮発性から非修飾に戻ることはできません。キャストを使用する必要があります:

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

揮発性修飾クラスは、そのインターフェースのサブセット、つまりクラス実装者の制御下にあるサブセットへのアクセスのみを許可します。ユーザーは、const_castを使用することによってのみ、そのタイプのインターフェースへのフルアクセスを取得できます。さらに、constnessと同様に、volatilenessはクラスからそのメンバーに伝播します(たとえば、volatileGadget.name_とvolatileGadget.state_はvolatile変数です)。

揮発性、クリティカルセクション、および競合状態

マルチスレッドプログラムで最も単純で最も頻繁に使用される同期デバイスはミューテックスです。ミューテックスは、取得プリミティブと解放プリミティブを公開します。あるスレッドでAcquireを呼び出すと、Acquireを呼び出す他のスレッドはブロックされます。後で、そのスレッドがReleaseを呼び出すと、Acquire呼び出しでブロックされたスレッドが1つだけ解放されます。つまり、特定のミューテックスに対して、Acquireの呼び出しとReleaseの呼び出しの間にプロセッサ時間を取得できるのは1つのスレッドだけです。Acquireの呼び出しとReleaseの呼び出しの間に実行されるコードは、クリティカルセクションと呼ばれます。(Windowsの用語は、ミューテックス自体をクリティカルセクションと呼ぶため、少し混乱しますが、「ミューテックス」は実際にはプロセス間ミューテックスです。スレッドミューテックスおよびプロセスミューテックスと呼ばれると便利です。)

ミューテックスは、競合状態からデータを保護するために使用されます。定義上、競合状態は、データに対するスレッドの増加の影響がスレッドのスケジュール方法に依存する場合に発生します。競合状態は、2つ以上のスレッドが同じデータの使用をめぐって競合する場合に発生します。スレッドは任意の時点で相互に割り込む可能性があるため、データが破損したり、誤って解釈されたりする可能性があります。したがって、データへの変更や場合によってはアクセスは、クリティカルセクションで慎重に保護する必要があります。オブジェクト指向プログラミングでは、これは通常、ミューテックスをメンバー変数としてクラスに格納し、そのクラスの状態にアクセスするたびにそれを使用することを意味します。

経験豊富なマルチスレッドプログラマーは、上記の2つの段落を読むことを諦めたかもしれませんが、揮発性の接続にリンクするため、彼らの目的は知的トレーニングを提供することです。これを行うには、C ++タイプの世界とスレッドセマンティクスの世界の間に類似点を描きます。

  • クリティカルセクションの外では、スレッドはいつでも他のスレッドを中断する可能性があります。制御がないため、複数のスレッドからアクセスできる変数は揮発性です。これは、揮発性の本来の目的、つまりコンパイラーが複数のスレッドによって使用される値を一度に無意識にキャッシュすることを防ぐという意図と一致しています。
  • ミューテックスによって定義されたクリティカルセクション内では、1つのスレッドのみがアクセスできます。その結果、クリティカルセクション内では、実行中のコードはシングルスレッドのセマンティクスを持ちます。制御変数は揮発性ではなくなりました—揮発性修飾子を削除できます。

つまり、スレッド間で共有されるデータは、概念的にはクリティカルセクションの外側では揮発性であり、クリティカルセクションの内側では不揮発性です。

ミューテックスをロックしてクリティカルセクションに入ります。const_castを適用して、型から揮発性修飾子を削除します。これらの2つの操作を組み合わせることができれば、C ++の型システムとアプリケーションのスレッドセマンティクスの間に接続を作成できます。コンパイラに競合状態をチェックさせることができます。

LockingPtr

ミューテックス取得とconst_castを収集するツールが必要です。揮発性オブジェクトobjとミューテックスmtxで初期化するLockingPtrクラステンプレートを開発しましょう。LockingPtrは、その存続期間中、mtxを取得し続けます。また、LockingPtrは、揮発性ストリップされたobjへのアクセスを提供します。アクセスは、operator->およびoperator *を介してスマートポインター方式で提供されます。const_castはLockingPtr内で実行されます。LockingPtrは、取得したミューテックスをその存続期間中保持するため、キャストは意味的に有効です。

まず、LockingPtrが機能するクラスMutexのスケルトンを定義しましょう。

class Mutex {
public:
    void Acquire();
    void Release();
    ...    
};

LockingPtrを使用するには、オペレーティングシステムのネイティブデータ構造とプリミティブ関数を使用してミューテックスを実装します。

LockingPtrは、制御変数のタイプでテンプレート化されています。たとえば、ウィジェットを制御する場合は、volatileWidgetタイプの変数で初期化するLockingPtrを使用します。

LockingPtrの定義は非常に単純です。LockingPtrは、洗練されていないスマートポインターを実装します。const_castとクリティカルセクションの収集にのみ焦点を当てています。

template <typename T>
class LockingPtr {
public:
    // Constructors/destructors
    LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {    
        mtx.Lock();    
    }
    ~LockingPtr() {    
        pMtx_->Unlock();    
    }
    // Pointer behavior
    T& operator*() {    
        return *pObj_;    
    }
    T* operator->() {   
        return pObj_;   
    }
private:
    T* pObj_;
    Mutex* pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr& operator=(const LockingPtr&);
};

LockingPtrは、その単純さにもかかわらず、正しいマルチスレッドコードを作成するのに非常に役立ちます。スレッド間で共有されるオブジェクトを揮発性として定義し、const_castを使用しないでください。常にLockingPtr自動オブジェクトを使用してください。これを例で説明しましょう。

ベクトルオブジェクトを共有する2つのスレッドがあるとします。

class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

スレッド関数内では、LockingPtrを使用して、buffer_メンバー変数への制御されたアクセスを取得するだけです。

void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        ... use *i ...
    }
}

コードの記述と理解は非常に簡単です。buffer_を使用する必要がある場合は常に、それを指すLockingPtrを作成する必要があります。これを行うと、ベクターのインターフェース全体にアクセスできるようになります。

良い点は、間違いを犯した場合、コンパイラがそれを指摘することです。

void SyncBuf::Thread2() {
    // Error! Cannot access 'begin' for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access 'end' for a volatile object
    for ( ; i != lpBuf->end(); ++i ) {
        ... use *i ...
    }
}

const_castを適用するか、LockingPtrを使用するまで、buffer_の関数にアクセスすることはできません。違いは、LockingPtrがconst_castを揮発性変数に適用する順序付けられた方法を提供することです。

LockingPtrは非常に表現力豊かです。1つの関数のみを呼び出す必要がある場合は、名前のない一時的なLockingPtrオブジェクトを作成し、それを直接使用できます。

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}

プリミティブ型に戻る

揮発性が制御されていないアクセスからオブジェクトを保護する方法と、LockingPtrがスレッドセーフなコードを記述する簡単で効果的な方法を提供する方法を見てきました。ここで、volatileによって異なる方法で処理されるプリミティブ型に戻りましょう。

複数のスレッドがint型の変数を共有する例を考えてみましょう。

class Counter {
public:
    ...
    void Increment() { ++ctr_; }
    void Decrement() { —ctr_; }
private:
    int ctr_;
};

インクリメントとデクリメントが異なるスレッドから呼び出される場合、上記のフラグメントはバグがあります。まず、ctr_は揮発性でなければなりません。第二に、++ ctr_のような一見アトミックな操作でさえ、実際には3段階の操作です。メモリ自体には算術機能はありません。変数をインクリメントする場合、プロセッサは次のことを行います。

  • その変数をレジスターで読み取ります
  • レジスタの値をインクリメントします
  • 結果をメモリに書き戻します

この3段階の操作は、RMW(Read-Modify-Write)と呼ばれます。RMW操作の変更部分では、ほとんどのプロセッサがメモリバスを解放して、他のプロセッサにメモリへのアクセスを許可します。

その時点で別のプロセッサが同じ変数に対してRMW操作を実行すると、競合状態が発生します。2回目の書き込みで最初の書き込みの効果が上書きされます。

これを回避するには、ここでもLockingPtrに依存します。

class Counter {
public:
    ...
    void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
    void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
    volatile int ctr_;
    Mutex mtx_;
};

これでコードは正しくなりましたが、SyncBufのコードと比較すると品質が劣っています。どうして?Counterを使用すると、誤ってctr_に直接(ロックせずに)アクセスしても、コンパイラは警告を表示しません。生成されたコードは単に正しくありませんが、ctr_が揮発性の場合、コンパイラは++ ctr_をコンパイルします。コンパイラはもはやあなたの味方ではなく、あなたの注意だけが競合状態を回避するのに役立ちます。

それならどうしますか?高レベルの構造で使用するプリミティブデータをカプセル化し、それらの構造でvolatileを使用するだけです。逆説的ですが、最初はvolatileの使用目的であったにもかかわらず、組み込みでvolatileを直接使用する方が悪いです。

揮発性メンバー関数

これまで、揮発性データメンバーを集約するクラスがありました。次に、より大きなオブジェクトの一部となり、スレッド間で共有されるクラスの設計について考えてみましょう。ここで、揮発性メンバー関数が非常に役立ちます。

クラスを設計するときは、スレッドセーフなメンバー関数のみを揮発性修飾します。外部からのコードがいつでも任意のコードから揮発性関数を呼び出すと想定する必要があります。忘れないでください:volatileは無料のマルチスレッドコードに等しく、クリティカルセクションはありません。不揮発性は、シングルスレッドシナリオまたはクリティカルセクション内に相当します。

たとえば、スレッドセーフなものと高速で保護されていないものの2つのバリアントで操作を実装するクラスウィジェットを定義します。

class Widget {
public:
    void Operation() volatile;
    void Operation();
    ...
private:
    Mutex mtx_;
};

オーバーロードの使用に注意してください。これで、ウィジェットのユーザーは、揮発性オブジェクトに対してスレッドセーフを取得するか、通常のオブジェクトに対して速度を取得するために、統一された構文を使用して操作を呼び出すことができます。ユーザーは、共有ウィジェットオブジェクトを揮発性として定義することに注意する必要があります。

揮発性メンバー関数を実装する場合、最初の操作は通常、これをLockingPtrでロックすることです。次に、非揮発性の兄弟を使用して作業を行います。

void Widget::Operation() volatile {
    LockingPtr<Widget> lpThis(*this, mtx_);
    lpThis->Operation(); // invokes the non-volatile function
}

概要

マルチスレッドプログラムを作成するときは、volatileを有利に使用できます。次のルールに従う必要があります。

  • すべての共有オブジェクトを揮発性として定義します。
  • プリミティブ型で直接volatileを使用しないでください。
  • 共有クラスを定義するときは、揮発性メンバー関数を使用してスレッドセーフを表現します。

これを実行し、単純な汎用コンポーネントLockingPtrを使用すると、スレッドセーフなコードを記述でき、競合状態について心配する必要がなくなります。コンパイラーが心配し、間違っている箇所を熱心に指摘するからです。

私が関わったいくつかのプロジェクトでは、volatileとLockingPtrを使用して大きな効果を上げています。コードはクリーンで理解しやすいです。いくつかのデッドロックを思い出しますが、デバッグが非常に簡単なため、競合状態よりもデッドロックの方が好きです。競合状態に関連する問題は事実上ありませんでした。しかし、あなたは決して知りません。

謝辞

洞察に満ちたアイデアを手伝ってくれたJamesKanzeとSorinJianuに感謝します。


Andrei Alexandrescuは、ワシントン州シアトルを拠点とするRealNetworks Inc.(www.realnetworks.com)の開発マネージャーであり、高く評価されている本 『Modern C ++ Design』の著者です。彼はwww.moderncppdesign.comで連絡されるかもしれません。Andreiは、C ++セミナー(www.gotw.ca/cpp_seminar)の注目のインストラクターの1人でもあります。

この記事は少し古いかもしれませんが、コンパイラーに競合状態をチェックさせながらイベントを非同期に保つのに役立つマルチスレッドプログラミングの使用でvolatile修飾子を使用する優れた使用法についての良い洞察を提供します。これは、メモリフェンスの作成に関するOPの元の質問に直接答えることはできないかもしれませんが、マルチスレッドアプリケーションで作業するときのvolatileの適切な使用に関する優れたリファレンスとして、他の人への回答としてこれを投稿することにしました。


0

キーワードはvolatile基本的に、オブジェクトの読み取りと書き込みは、プログラムによって記述されたとおり実行する必要があり、決して最適化しないことを意味します。バイナリコードは、CまたはC ++コードに従う必要があります。これが読み取られるロード、書き込みがあるストアです。

また、読み取りによって予測可能な値が得られるとは考えられないことも意味します。コンパイラは、同じ揮発性オブジェクトへの書き込みの直後であっても、読み取りについて何も想定してはなりません。

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatile「Cは高レベルのアセンブリ言語です」ツールボックスで最も重要なツールである可能性があります。

オブジェクトを揮発性として宣言するだけで、非同期の変更を処理するコードの動作を保証するのに十分かどうかは、プラットフォームによって異なります。CPUが異なれば、通常のメモリの読み取りと書き込みに対して異なるレベルの同期が保証されます。この分野の専門家でない限り、このような低レベルのマルチスレッドコードを作成しようとしないでください。

アトミックプリミティブは、マルチスレッド用のオブジェクトの優れた高レベルのビューを提供し、コードについての推論を容易にします。ほとんどすべてのプログラマーは、アトミックプリミティブ、またはミューテックス、読み取り/書き込みロック、セマフォ、その他のブロッキングプリミティブなどの相互排除を提供するプリミティブのいずれかを使用する必要があります。

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