C ++ 11では、通常はvolatile
スレッド化に使用せず、MMIOにのみ使用します
しかしTL:DRは、mo_relaxed
コヒーレントキャッシュ(つまりすべて)を備えたハードウェア上でアトミックに「機能」します。レジスタに変数を保持しているコンパイラを停止するだけで十分です。 atomic
原子性やスレッド間の可視性を作成するためにメモリバリアは必要ありません。現在のスレッドを操作の前後に待機させて、このスレッドの異なる変数へのアクセス間の順序を作成します。 mo_relaxed
ロード、ストア、またはRMWを行うだけで、バリアは必要ありません。
ロール自分で自分とアトミックについてvolatile
(や障壁のためのインラインASM)C ++ 11の前に悪い昔std::atomic
、volatile
仕事にいくつかのことを取得する唯一の良い方法でした。しかし、それは実装がどのように機能するかについての多くの仮定に依存しており、いかなる標準によっても保証されていませんでした。
たとえば、Linuxカーネルは引き続き独自のハンドリングされたアトミックをvolatile
で使用しますが、特定のC実装(GNU C、clang、および多分ICC)のみをサポートします。これは、GNU Cの拡張機能とインラインasmの構文とセマンティクスが原因である場合もありますが、コンパイラの動作に関するいくつかの仮定に依存しているためでもあります。
ほとんどの場合、新しいプロジェクトでは間違った選択です。std::atomic
(with std::memory_order_relaxed
)を使用して、コンパイラーでと同じ効率的なマシンコードを生成できますvolatile
。 スレッド化の目的std::atomic
でmo_relaxed
廃止さvolatile
れました。(一部のコンパイラでatomic<double>
、最適化に失敗したバグを回避することを除いて。)
std::atomic
メインストリームコンパイラ(gccやclangなど)の内部実装は、内部で使用するだけではありませんvolatile
。コンパイラーは、アトミックなロード、ストア、およびRMW組み込み関数を直接公開します。(例えば、「プレーン」オブジェクトで動作するGNU C __atomic
ビルトイン。)
揮発性は実際に使用可能です(ただし、使用しないでください)
とvolatile
はいえ、exit_now
CPUがどのように動作するか(コヒーレントキャッシュ)、どのようにvolatile
動作するかについての共有された仮定のため、実際のCPU上の既存のすべてのC ++実装のフラグ(?)のようなものに実際に使用できます。しかし、それほど多くはなく、推奨されません。 この回答の目的は、既存のCPUとC ++実装が実際にどのように機能するかを説明することです。あなたがそれを気にしないなら、あなたが知る必要があるのは、スレッドstd::atomic
化のvolatile
ためにmo_relaxedが廃止されたことです。
(ISO C ++標準はかなり曖昧であり、volatile
アクセスはC ++抽象マシンのルールに従って厳密に評価されるべきであり、最適化されるべきではないと述べています。実際の実装ではC ++アドレス空間をモデル化するためにマシンのメモリアドレス空間を使用するため、つまりvolatile
、メモリ内のオブジェクト表現にアクセスするには、読み取りと割り当てをコンパイルして命令をロード/ストアする必要があります。)
別の答えが指摘するように、exit_now
フラグは同期を必要としないスレッド間通信の単純なケースです。配列の内容が準備できていることなどは公開されていません。別のスレッドでの最適化されていないロードによって即座に気づかされるだけのストア。
// global
bool exit_now = false;
// in one thread
while (!exit_now) { do_stuff; }
// in another thread, or signal handler in this thread
exit_now = true;
揮発性またはアトミックがない場合、as-ifルールおよびデータ競合UBがないという仮定により、コンパイラーは、無限ループに入る(または入る)前にフラグを1回だけチェックするasmに最適化できます。これは、実際のコンパイラで実際に起こることです。(そしてdo_stuff
、ループは決して終了しないため、通常はそのほとんどを最適化します。そのため、ループに入ると、結果を使用した可能性のあるそれ以降のコードには到達できません)。
// Optimizing compilers transform the loop into asm like this
if (!exit_now) { // check once before entering loop
while(1) do_stuff; // infinite loop
}
マルチスレッド化プログラムは最適化モードでスタックしますが、-O0で正常に実行されます。これは、x86-64のGCCでこれがどのように発生するかを示す例です(GCCのasm出力の説明付き)。また、MCUプログラミング-C ++ O2の最適化は、electronics.SEのループで中断しますが、別の例を示しています。
通常、CSEとホイストがグローバル変数を含むループからロードする積極的な最適化が必要です。
C ++ 11以前volatile bool exit_now
は、これを意図したとおりに機能させる1つの方法でした(通常のC ++実装で)。ただし、C ++ 11では、データレースUBが引き続き適用されるvolatile
ため、ハードウェアコヒーレントキャッシュを想定している場合でも、ISO規格によってすべての場所での動作が保証されるわけではありません。
幅の広いタイプの場合volatile
、ティアリングがないことは保証されないことに注意してください。これはbool
通常の実装では問題ではないため、ここではその区別を無視しました。しかし、それvolatile
は、緩和されたアトミックと同等ではなく、依然としてデータ競合UBの影響を受ける理由の一部でもあります。
「意図したとおり」とはexit_now
、他のスレッドが実際に終了するのを待機しているスレッドを意味するものではないことに注意してください。あるいはexit_now=true
、このスレッドで後の操作を続行する前に、揮発性ストアがグローバルに表示されるまで待機します。(atomic<bool>
デフォルトでmo_seq_cst
は、少なくともseq_cstがロードされる前に待機します。多くのISAでは、ストアの後に完全なバリアを取得します)。
C ++ 11は、同じをコンパイルする非UBの方法を提供します
「実行を続ける」または「今すぐ終了」フラグはstd::atomic<bool> flag
、mo_relaxed
使用する
flag.store(true, std::memory_order_relaxed)
while( !flag.load(std::memory_order_relaxed) ) { ... }
から得られるのとまったく同じasm(高価なバリア命令なし)が得られvolatile flag
ます。
ティアリングなしだけでなく、atomic
UBなしで1つのスレッドに格納して別のスレッドにロードする機能も提供するため、コンパイラーはループからロードを引き上げることができません。(データ競合UBがないという前提により、非アトミックな非揮発性オブジェクトに対して積極的な最適化が可能になります。)この機能atomic<T>
volatile
は、純粋なロードおよび純粋なストアのほとんど同じです。
atomic<T>
また作る +=
アトミックRMW操作のなどます(一時、操作、別のアトミックストアへのアトミックロードよりも大幅に高価です。アトミックRMWが必要ない場合は、ローカル一時コードでコードを記述してください)。
seq_cst
あなたが得るだろうデフォルトの順序でwhile(!flag)
、rtrによる順序付けの保証も追加されます。非アトミックアクセス、および他のアトミックアクセス。
(理論的には、ISO C ++標準はアトミックのコンパイル時の最適化を除外していません。しかし、実際には、それが問題にならない場合を制御する方法がないため、コンパイラーはそうではありvolatile atomic<T>
ません。コンパイラーが最適化した場合、アトミックの最適化を十分に制御できるため、現時点ではコンパイラーは最適化しません。コンパイラーが冗長std :: atomic書き込みをマージしないのはなぜですか?volatile atomic
現在のコード でwg21 / p0062を使用しないことをお勧めします。アトミック。)
volatile
これは実際のCPUで実際に機能します(ただし、まだ使用していません)
弱い順序付けのメモリモデル(x86以外)でも。しかし、実際には使用せずatomic<T>
、mo_relaxed
代わりに使用してください!! このセクションの目的は、実際のCPUがどのように機能するかについての誤解に対処することであり、正当化することではありませんvolatile
。ロックレスコードを記述している場合は、おそらくパフォーマンスを気にします。キャッシュとスレッド間通信のコストを理解することは、通常、優れたパフォーマンスにとって重要です。
実際のCPUには一貫したキャッシュ/共有メモリがあります。1つのコアからのストアがグローバルに可視になると、他のコアは古い値をロードできなくなります。 (神話プログラマーが Javaの揮発性について説明しているCPUキャッシュについて信じるatomic<T>
、seq_cstメモリー順序のC ++に相当するも参照してください。)
私が言うときの負荷を、私はメモリをアクセスするのasm命令を意味します。これはvolatile
アクセスが保証するものであり、非アトミック/非揮発性C ++変数の左辺値から右辺値への変換と同じものではありません。(local_tmp = flag
またはwhile(!flag)
)。
あなたが敗北する必要がある唯一のものは、最初のチェックの後にまったくリロードしないコンパイル時の最適化です。順序付けなしで、各反復でのロード+チェックで十分です。このスレッドとメインスレッド間の同期がなければ、ストアがいつ発生したか、またはロードwrtの順序がいつ起こったのかを説明しても意味がありません。ループ内の他の操作。このスレッドに表示されている場合にのみ重要です。exit_nowフラグが設定されていることを確認したら、終了します。典型的なx86 Xeonのコア間レイテンシは、個別の物理コア間で40ns程度になる可能性があります。
理論的には、コヒーレントキャッシュのないハードウェア上のC ++スレッド
プログラマーがソースコードで明示的なフラッシュを行う必要がない純粋なISO C ++だけで、これがリモートで効率的になる方法はありません。
理論的には、これとは異なるマシンでC ++実装を行うことができます。他のコアの他のスレッドから物事を見えるようにするには、コンパイラーが生成した明示的なフラッシュが必要です。(または、読み取りが多分古いコピーを使用しないようにするため)。C ++標準ではこれは不可能ではありませんが、C ++のメモリモデルは、コヒーレントな共有メモリマシンで効率的になるように設計されています。たとえば、C ++標準では、「読み取り-読み取りコヒーレンス」、「書き込み-読み取りコヒーレンス」などについても説明されています。標準の1つの注記では、ハードウェアへの接続も示しています。
http://eel.is/c++draft/intro.races#19
[注:上記の4つの一貫性要件により、両方の操作が緩和されたロードであっても、アトミック操作を単一のオブジェクトに再配列するコンパイラーは事実上許可されません。これにより、ほとんどのハードウェアで提供されるキャッシュコヒーレンス保証が、C ++のアトミック操作で使用できるようになります。—エンドノート]
release
自分自身といくつかの選択したアドレス範囲のみをフラッシュするストアのメカニズムはありません。取得ロードがこのリリースストアを見た場合、他のスレッドが何を読みたいのかわからないため、すべてを同期する必要があります。スレッド間で発生前の関係を確立するrelease-sequence。これにより、書き込みスレッドによって行われた以前の非アトミック操作が安全に読み取れるようになります。リリースストア後にさらに書き込みを行わない限り...)または、コンパイラはほんの少しのキャッシュラインだけがフラッシュを必要とすることを証明するために本当に賢くなります。
関連:NUMAではmov + mfenceは安全ですか?コヒーレントな共有メモリがないx86システムが存在しないことについて詳しく説明します。また関連:同じ場所へのロード/ストアの詳細については、ARMでのロードとストアの並べ替え。
一貫性のない共有メモリを持つクラスターがあると思いますが、それらは単一システムイメージマシンではありません。各コヒーレンシドメインは個別のカーネルを実行するため、単一のC ++プログラムのスレッドを実行することはできません。代わりに、プログラムの個別のインスタンスを実行します(それぞれに独自のアドレススペースがあります。1つのインスタンスのポインターは、他のインスタンスでは無効です)。
それらが明示的なフラッシュを介して互いに通信するようにするには、通常、MPIまたは他のメッセージ受け渡しAPIを使用して、フラッシュする必要があるアドレス範囲をプログラムに指定します。
実際のハードウェアは、std::thread
キャッシュの一貫性の境界を越えて実行されません。
いくつかの非対称ARMチップが存在し、物理アドレス空間は共有されていますが、内部共有可能なキャッシュドメインはありません。首尾一貫していない。(たとえば、コメントスレッド A8コアとTI Sitara AM335xのようなCortex-M3)。
ただし、これらのコアでは異なるカーネルが実行され、両方のコアでスレッドを実行できる単一のシステムイメージではありません。std::thread
コヒーレントキャッシュなしでCPUコア全体でスレッドを実行するC ++実装については知りません。
特にARMの場合、GCCとclangは、すべてのスレッドが同じ内部共有可能ドメインで実行されることを想定してコードを生成します。実際、ARMv7 ISAマニュアルには、
このアーキテクチャ(ARMv7)は、同じオペレーティングシステムまたはハイパーバイザーを使用するすべてのプロセッサが同じ内部共有可能共有可能性ドメインにあることを想定して記述されています
したがって、別々のドメイン間の非コヒーレントな共有メモリは、異なるカーネル下の異なるプロセス間の通信に共有メモリ領域を明示的にシステム固有に使用するためのものにすぎません。
このコンパイラでのdmb ish
(内部共有可能バリア)とdmb sy
(システム)メモリバリアを使用したコード生成に関するこのCoreCLRの説明も参照してください。
他のISAのC ++実装は、std::thread
一貫性のないキャッシュを使用するコア全体で実行されないという主張をします。 そのような実装が存在しないという証拠はありませんが、その可能性は非常に低いようです。そのように機能する特定のエキゾチックなハードウェアを対象としない限り、パフォーマンスについての考えは、すべてのスレッド間のMESIのようなキャッシュコヒーレンシを想定する必要があります。(atomic<T>
ただし、正確性を保証する方法で使用することをお勧めします!)
コヒーレントキャッシュはそれを簡単にします
ただし、コヒーレントキャッシュを備えたマルチコアシステムでは、リリースストアを実装することは、明示的なフラッシュを行わずに、このスレッドのストアのコミットをキャッシュに順序付けることを意味します。(https://preshing.com/20120913/acquire-and-release-semantics/およびhttps://preshing.com/20120710/memory-barriers-are-like-source-control-operations/)。(そして、acquire-loadは、他のコアのキャッシュへのアクセスを注文することを意味します)。
メモリバリア命令は、ストアバッファが空になるまで、現在のスレッドのロードやストアをブロックするだけです。それは常にそれ自体で可能な限り速く発生します。 (メモリバリアはキャッシュコヒーレンスが完了したことを保証しますか?この誤解に対処します)。したがって、順序付けが必要ない場合は、他のスレッドで表示を確認するだけで十分ですmo_relaxed
。(そしてそうですがvolatile
、そうしないでください。)
プロセッサーへのC / C ++ 11マッピング
も参照してください。
おもしろい事実:x86では、すべてのasmストアはリリースストアです。これは、x86メモリモデルが基本的にseq-cstとストアバッファー(ストアフォワーディング付き)であるためです。
半関連re:ストアバッファー、グローバルな可視性、一貫性:C ++ 11はほとんど保証しません。ほとんどの実際のISA(PowerPCを除く)は、他の2つのスレッドによる2つのストアの出現順序にすべてのスレッドが同意できることを保証します。(正式なコンピュータアーキテクチャメモリモデルの用語では、「マルチコピーアトミック」です)。
別の誤解は、他のコアがストアを表示するためにストアバッファーをフラッシュするために、メモリーフェンスasm命令が必要であるということです。実際には、ストアバッファーは常にできるだけ早く自身を排出(L1dキャッシュにコミット)しようとします。そうしないと、いっぱいになって実行が停止します。完全なバリア/フェンスは、ストアバッファーが空になるまで現在のスレッドを停止するので、後のロードは前のストアの後にグローバルな順序で表示されます。
(x86の者に強く命じASMメモリモデル手段があることvolatile
のx86に近いあなたを与えてしまうことがありmo_acq_rel
、そのコンパイル時を除き、非アトミック変数で並べ替えがまだ発生することがあります。しかし、ほとんどが非X86ので、弱命じたメモリモデルを持っている、volatile
とrelaxed
などについてですmo_relaxed
可能な限り弱い。)