「揮発性」は、マルチコアシステムのポータブルCコードで何かを保証しますか?


12

見た後、 他の 質問 その 回答を、私はという印象を得るCで「揮発性」というキーワードが正確に何を意味するのかには広範な合意はありませんが。

標準自体でさえ、誰もがそれ何を意味するのかについて合意するのに十分明確ではないようです。

他の問題の中で:

  1. それはあなたのハードウェアとあなたのコンパイラに依存して異なる保証を提供するようです。
  2. コンパイラの最適化には影響しますが、ハードウェアの最適化には影響しません。そのため、独自のランタイム最適化を行う高度なプロセッサでは、コンパイラ防止したい最適化を防止できるかどうかさえ明確ではありません。(一部のコンパイラーは、一部のシステムで一部のハードウェア最適化を防止するために命令を生成しますが、これは決して標準化されていないようです。)

問題を要約すると、「大量に読み取った後」「揮発性」が次のようなことを保証しているように見えます。値は、レジスタからだけでなく、少なくともコアのL1キャッシュにも、同じ順序で読み書きされます。読み取り/書き込みがコードに表示されます。しかし、これは役に立たないようです。レジスタからの読み取り/レジスタへの書き込みは同じスレッド内ですでに十分ですが、L1キャッシュとの調整は、他のスレッドとの調整に関してそれ以上何も保証しません。L1キャッシュとだけ同期することがいつ重要になるかは想像できません。

USE 1
揮発性の広く合意された使用は、ライトを(直接、ハードウェアで)制御するメモリ内のビットのように、特定のメモリ位置がI / O機能にハードウェアでマッピングされている古いシステムまたは組み込みシステムであるようです、またはキーボードのキーが押されているかどうかを通知するメモリ内のビット(ハードウェアによって直接キーに接続されているため)。

と思われる「利用1」とは、そのターゲットのマルチコアシステムを含んポータブルなコードでは発生しません。

USE 2
「use 1」とそれほど変わらないのは、割り込みハンドラー(ライトの制御やキーからの情報の保存など)によっていつでも読み書きできるメモリです。しかし、すでにこのため、システムによっては、割り込みハンドラ 独自のメモリキャッシュを備えた別のコア実行される可能性があり、「揮発性」はすべてのシステムでキャッシュの一貫性を保証しないという問題があります。

したがって、「use 2」は「volatile」が提供できる範囲を超えているようです。

USE 3
他に議論の余地のない唯一の用途は、コンパイラーが認識しない同じメモリーを指している同じメモリーを指すさまざまな変数を介したアクセスの誤最適化を防ぐことです。しかし、人々がそれについて話していないので、これはおそらく議論の余地がないだけです-私はそれについての言及を1つだけ見ました。また、C標準では、「異なる」ポインター(関数への異なる引数など)が同じ項目または近くの項目を指す可能性があることをすでに認識しており、コンパイラーがそのような場合でも機能するコードを生成する必要があることをすでに指定しています。しかし、私はこのトピックを最新(500ページ)の標準ですぐに見つけることができませんでした。

では「use 3」はまったく存在しないのでしょうか。

したがって、私の質問:

「揮発性」は、マルチコアシステムのポータブルCコードで何かを保証しますか?


編集-更新

最新の標準を参照した後、答えは少なくとも非常に制限されているように見えます:
1.標準は、特定のタイプ "volatile sig_atomic_t"の特別な扱いを繰り返し指定しています。ただし、この規格では、マルチスレッドプログラムでシグナル関数を使用すると、未定義の動作が発生することも規定されています。したがって、この使用例は、シングルスレッドプログラムとそのシグナルハンドラ間の通信に限定されているようです。
2.この規格では、setjmp / longjmpに関連する「揮発性」の明確な意味も規定されています。(重要なコードの例は、他の質問回答に記載されています。)

したがって、より正確な質問は次のようになります
。「揮発性」は、(1)シングルスレッドプログラムがシグナルハンドラから情報を受信できるようにする、または(2)setjmpを許可することを除いて、マルチコアシステムのポータブルCコードで何でも保証しますかsetjmpとlongjmpの間で変更された変数を表示するコード?

これははい/いいえの質問です。

「はい」の場合、「揮発性」が省略された場合にバグになるバグのないポータブルコードの例を示すことができればすばらしいでしょう。「いいえ」の場合、マルチコアターゲットの場合、これら2つの非常に特殊なケース以外では、コンパイラーは「揮発性」を無視してもかまいません。


3
シグナルはポータブルCに存在します。シグナルハンドラーによって更新されるグローバル変数はどうですか?これはvolatile、プログラムが非同期に変更される可能性があることをプログラムに通知する必要があります。
Nate Eldredge

2
@NateEldredgeグローバルは、揮発性だけでは十分ではありません。それもアトミックである必要があります。
Eugene Sh。

@EugeneSh .:はい、もちろん。しかし、当面の問題はvolatile具体的には、それが必要だと私は信じています。
Nate Eldredge、

" L1キャッシュとの調整は、他のスレッドとの調整に関してこれ以上何も保証しません " 他のスレッドと通信するのに十分ではない "L1キャッシュとの調整"はどこにありますか?
curiousguy

1
多分、関連性があり揮発性を非推奨にするC ++提案、この提案はあなたがここで提起する多くの懸念に対処し、おそらくその結果はC委員会に影響を与えるでしょう
MM

回答:


1

問題を要約すると、「大量に読み取った後」「揮発性」が次のようなことを保証しているように見えます。値は、レジスタからだけでなく、少なくともコアのL1キャッシュにも、同じ順序で読み書きされます。読み取り/書き込みはコードに表示されます

いいえ、絶対にありません。そして、MTセーフコードの目的では、volatileはほとんど役に立たなくなります。

もしそうなら、揮発性は複数のスレッドで共有される変数に非常に適しています。L1キャッシュでのイベントの順序付けは、協調可能な標準的なCPU(マルチコアまたはマザーボード上のマルチCPU)で実行する必要があるすべてです。 C / C ++またはJavaマルチスレッディングの通常の実装を、一般的な予想コスト(つまり、ほとんどのアトミックまたは非コンテンツのmutex操作ではそれほど大きなコストではない)で可能にする方法で。

しかし、volatileは、理論上または実際のいずれにおいても、キャッシュ内の保証された順序付け(または「メモリの可視性」)を提供しませ

(注:以下は、標準ドキュメントの適切な解釈、標準の意図、歴史的実践、およびコンパイラ作成者の期待の深い理解に基づいています。このアプローチは、歴史、実際の実践、および実在の人物の期待と理解に基づいています現実の世界。これは、優れた仕様書であることが知られていない文書の単語を解析するよりもはるかに強力で信頼性が高く、何度も改訂されています。)

実際には、volatileは、実行中のプログラムのデバッグ情報をあらゆる最適化レベルで使用する機能であるptrace-abilityを保証します。また、デバッグ情報がこれらのvolatileオブジェクトに対して意味をなすという事実も保証します。

  • ptrace(ptraceのようなメカニズム)を使用して、揮発性オブジェクトが関係する操作の後にシーケンスポイントに意味のあるブレークポイントを設定できます:実際にこれらのポイントで正確にブレークできます(これは、多くのブレークポイントを設定する場合にのみ機能することに注意してください) C / C ++ステートメントは、大規模に展開されたループのように、多くの異なるアセンブリの開始点と終了点にコンパイルできます);
  • 実行のスレッドが停止している間は、すべての揮発性オブジェクトの値を読み取ることができます。これは、それらが標準的な表現を持っているためです(それぞれのタイプのABIに従います)。非揮発性ローカル変数は、非定型表現f.exを持つことができます。シフトされた表現:配列のインデックス付けに使用される変数は、インデックス付けを容易にするために、個々のオブジェクトのサイズで乗算される場合があります。または、(同様に変換された変数のすべての使用が続く限り)配列要素へのポインタで置き換えられる可能性があります(整数でdxをduに変更することを考えてください)。
  • また、これらのオブジェクトを変更することもできます(メモリマッピングで許可されている限り、const修飾された静的存続期間を持つ揮発性オブジェクトは、メモリ範囲にマップされた読み取り専用であるため)。

実際の揮発性保証は、厳密なptraceの解釈より少し多いです。また、揮発性自動変数がスタックにアドレスを持っていることを保証します。これは、レジスターに割り当てられないため、ptrace操作をよりデリケートにするレジスター割り当てです(コンパイラーは変数がレジスタに割り当てられる方法を説明するデバッグ情報を出力しますが、レジスタの状態の読み取りと変更は、メモリアドレスへのアクセスよりも少し複雑です)。

すべての変数を少なくともシーケンスポイントで揮発性と見なす完全なプログラムデバッグ機能は、コンパイラーの「ゼロ最適化」モードによって提供されることに注意してください。このモードは、算術単純化のような些細な最適化を実行します(通常、すべてのモードでの最適化)。ただし、揮発性は非最適化よりも強力です。揮発性オブジェクトではなく、x-x不揮発性整数に対して単純化できxます。

そのため、システムコールのコンパイラによるソースからバイナリ/アセンブリへの変換は、コンパイラによる再解釈、変更、または最適化ではないなど、揮発性はそのままコンパイルされることが保証されています。ライブラリ呼び出しはシステム呼び出しである場合とそうでない場合があることに注意してください。多くの公式のシステム関数は実際にはライブラリ関数であり、割り込みの薄い層を提供し、通常は最後にカーネルに委ねます。(特にgetpid、カーネルに移動する必要がなく、情報を含むOSによって提供されるメモリ位置を読み取ることができます。)

揮発性の相互作用は、「抽象的な機械」に従う必要のある実際の機械の外界との相互作用です。これらは、プログラム部分と他のプログラム部分との内部的な相互作用ではありません。コンパイラーは、それが知っていること、つまり内部プログラム部分についてのみ推論できます。

揮発性アクセスのコード生成は、そのメモリ位置との最も自然な相互作用に従う必要があります。当然のことです。ことを意味することにいくつかの揮発性のアクセスがアトミックであることが予想されるが:の表現読み書きする自然な方法ならばlongアーキテクチャ上は原子である、の読み取りまたは書き込みをすることを期待されますvolatile long、アトミックになり、コンパイラが生成するべきではありませんたとえば、揮発性オブジェクトにバイト単位でアクセスするためのばかげた非効率的なコード

アーキテクチャを理解することでそれを判断できるはずです。揮発性とはコンパイラが透過的であることを意味するため、コンパイラについて何も知る必要はありません。

しかし、volatileは、特定の場合に最適化された最小限のメモリの操作を実行するために、予想されるアセンブリの出力を強制するだけです。

一般的なケースは、コンストラクトに関する情報がない場合にコンパイラーが行うことです:f.ex。ダイナミックディスパッチを介して左辺値の仮想関数を呼び出すのは一般的なケースであり、コンパイル時に式で指定されたオブジェクトのタイプを特定した後、オーバーライドを直接呼び出します。コンパイラーは常にすべての構造の一般的なケース処理を行い、ABIに従います。

揮発性スレッドを同期又は「メモリの可視性」を提供するために特別なものはない:揮発性は、抽象レベルで保証を提供しない実行スレッド内部から見または停止、つまり、CPUコアの内部

  • volatileは、どのメモリ操作がメインRAMに到達するかについては何も述べていません(これらの保証を得るために、アセンブリ命令またはシステムコールで特定のメモリキャッシングタイプを設定できます)。
  • volatileは、メモリ操作がいつでも(L1でも)あらゆるレベルのキャッシュにコミットされることを保証しません

2番目の点のみが、揮発性はほとんどのスレッド間通信の問題で役に立たないことを意味します。最初の点は、CPU外のハードウェアコンポーネントとの通信を含まず、メモリバス上にあるプログラミングの問題には本質的に無関係です。

スレッドを実行するコアの観点から保証された動作を提供する揮発性のプロパティは、そのスレッドの実行順序の観点から実行される、そのスレッドに配信される非同期信号が、ソースコードの順序で操作を表示することを意味します。

スレッドにシグナルを送信することを計画していない限り(以前に合意された停止ポイントがない現在実行中のスレッドに関する情報を統合するための非常に有用なアプローチ)、volatileは適していません。


6

私は専門家ではありませんが、cppreference.comには、に関するvolatileかなり良い情報のように思えるものがあります。その要点は次のとおりです。

volatile修飾型の左辺値式を介して行われるすべてのアクセス(読み取りと書き込みの両方)は、最適化のために観察可能な副作用と見なされ、抽象マシンの規則に従って厳密に評価されます(つまり、すべての書き込みは次のシーケンスポイントの前のある時間)。つまり、実行の単一スレッド内では、揮発性アクセスは、シーケンスポイントによって揮発性アクセスから分離されている別の目に見える副作用と比較して最適化したり、並べ替えたりすることはできません。

また、いくつかの用途があります。

揮発性物質の使用

1)静的な揮発性オブジェクトはメモリマップされたI / Oポートをモデル化し、静的なconst揮発性オブジェクトはリアルタイムクロックなどのメモリマップされた入力ポートをモデル化します

2)タイプsig_atomic_tの静的な揮発性オブジェクトは、シグナルハンドラーとの通信に使用されます。

3)setjmpマクロの呼び出しを含む関数に対してローカルな揮発性変数は、longjmpが戻った後も値を保持することが保証されている唯一のローカル変数です。

4)さらに、揮発性変数を使用して、特定の形式の最適化を無効にすることができます。たとえば、マイクロストアのデッドストアの削除や定数の折りたたみを無効にすることができます。

そしてもちろん、それはvolatileスレッドの同期には役に立たないと述べています:

揮発性変数はスレッド間の通信には適していません。原子性、同期、またはメモリの順序付けは提供されません。同期せずに別のスレッドによって変更された揮発性変数からの読み取り、または2つの非同期スレッドからの同時変更は、データ競合が原因で未定義の動作になります。


2
特に、(2)と(3)は移植可能なコードに関連しています。
Nate Eldredge

2
@TEDドメイン名にもかかわらず、リンクはC ++ではなくCに関する情報へのリンクです
David Brown

@NateEldredge longjmpC ++コードではめったに使用できません。
curiousguy

@DavidBrown CとC ++は、監視可能なSEの定義が同じで、スレッドプリミティブは基本的に同じです。
curiousguy

4

まず第一に、volatileアクセスや類似の意味のさまざまな解釈に関して、歴史的にさまざまな問題がありました。:この研究を参照してください。それについての揮発性物質のAre正しくコンパイルし、何を

その研究で言及されているさまざまな問題とは別に、の動作volatileは移植可能であり、それらの1つの側面、つまり、それらがメモリバリアとして機能する場合を 除きます。メモリバリアは、コードの同時シーケンス実行を防ぐために存在するメカニズムです。volatileメモリのバリアとして使用することは確かに移植可能ではありません。

C言語がメモリの動作を保証するかどうかvolatileは明らかに議論の余地がありますが、個人的には言語は明確だと思います。最初に、副作用の正式な定義があります。C175.1.2.3:

volatileオブジェクトへのアクセス、オブジェクトの変更、ファイルの変更、またはこれらの操作のいずれかを実行する関数の呼び出しはすべて、実行環境の状態の変化である副作用です。

標準では、評価の順序(実行)を決定する方法として、シーケンスという用語を定義しています。定義は形式的で扱いにくいものです。

前にシーケンス化されているのは、単一のスレッドによって実行された評価間の非対称で推移的なペアワイズ関係であり、これらの評価の間に部分的な順序が生じます。AとBの2つの評価が与えられた場合、AがBの前にシーケンスされる場合、Aの実行はBの実行の前に行われます(逆に、AがBの前にシーケンスされる場合、Bは Aの後にシーケンスされます。)Aがシーケンスされない場合Bの前または後に、AおよびBのシーケンス解除されます。評価AとBは、AがBの前または後にシーケンスされる場合、不確定にシーケンスされますが、どちらが指定されているかは不明です。13)シーケンスポイントの存在 式AとBの評価の間は、Aに関連付けられたすべての値の計算と副作用がBに関連付けられたすべての値の計算と副作用の前にシーケンス処理されることを意味します(シーケンスポイントの概要は付録Cに記載されています)。

TL;上記のDRは、我々は、発現有する場合と基本的にA副作用が含まれ、それは別の式の前に実行する行わなければならないB場合には、B後に配列決定されますA

Cコードの最適化は、この部分を通じて可能になります。

抽象マシンでは、すべての式がセマンティクスで指定されたとおりに評価されます。実際の実装では、その値が使用されておらず、必要な副作用(関数の呼び出しや揮発性オブジェクトへのアクセスによるものも含む)が発生していないと推定できる場合、式の一部を評価する必要はありません。

これは、プログラムが標準を他の場所で要求する順序(評価の順序など)で式を評価(実行)できることを意味します。ただし、使用されていないと推定できる場合は、値を評価(実行)する必要はありません。たとえば、演算で式0 * xを評価xして単純にに置き換える必要はありません0

変数へのアクセスが副作用でない限り。場合にという意味xvolatile、それがなければなりません(実行)評価0 * x結果は常に0最適化されますにもかかわらず、許可されていません。

さらに、標準は観察可能な行動について述べています:

適合実装の最小要件は次のとおりです。

  • 揮発性オブジェクトへのアクセスは、抽象マシンのルールに従って厳密に評価されます。
    /-/これはプログラムの観察可能な動作です。

上記のすべてをvolatile前提として、書かれたCソースのセマンティクスが別のことを言っている場合、準拠する実装(コンパイラ+基礎となるシステム)は、オブジェクトのアクセスを順序付けられていない順序で実行しない可能性があります。

つまり、この例では

volatile int x;
volatile int y;
z = x;
z = y;

どちらの代入式をしなければならない評価さとz = x; しなければならない前に評価されますz = y;。これらの2つの操作を2つの異なる非シーケンスコアにアウトソーシングするマルチプロセッサの実装は準拠していません。

ジレンマは、コンパイラーがプリフェッチキャッシングや命令のパイプライン処理などのことについて、特にOS上で実行されている場合は、ほとんど実行できないことです。そして、コンパイラーはその問題をプログラマーに引き渡して、メモリーの障壁が今やプログラマーの責任であることを彼らに伝えます。C標準では、問題はコンパイラーによって解決する必要があると明確に規定されています。

ただし、コンパイラは必ずしも問題を解決する必要はありません。そのvolatileため、メモリバリアとして機能するための移植性はありません。これは実装の品質の問題になっています。


@curiousguy関係ありません。
ランディン

@curiousguy修飾子の有無にかかわらず、ある種の整数型であれば問題ありません。
ランディン

それが単純な非揮発性整数である場合、冗長な書き込みzが実際に実行されるのはなぜですか?(のようにz = x; z = y;)値は次のステートメントで消去されます。
curiousguy

@curiousguy volatile変数への読み取りは、指定された順序で関係なく実行する必要があるためです。
ランディン

次に、z実際に2回割り当てられますか?「読み取りが実行された」ことをどうやって知っていますか?
curiousguy
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.