web-dev-qa-db-ja.com

取得/解放と逐次一貫性のあるメモリ順序

Tがプリミティブ型である任意のstd::atomic<T>の場合:

std::memory_order_acq_rel操作にfetch_xxxを使用し、load操作にstd::memory_order_acquireを使用し、store操作にstd::memory_order_releaseを盲目的に使用する場合(つまり、これらの関数のデフォルトのメモリ順序をリセットする)

  • 結果は、宣言された操作のいずれかにstd::memory_order_seq_cst(デフォルトとして使用されている)を使用した場合と同じになりますか?
  • 結果が同じである場合、この使用法は、効率の点でstd::memory_order_seq_cstを使用する場合とはどういうわけか異なりますか?
37
zahir

アトミック操作のC++ 11メモリ順序パラメータは、順序の制約を指定します。 std::memory_order_releaseでストアを実行し、別のスレッドからのロードがstd::memory_order_acquireで値を読み取る場合、2番目のスレッドからの後続の読み取り操作では、ストアのリリース前の最初のスレッドによって任意のメモリ位置に格納された値が表示されますまたは後でそれらのメモリ位置のいずれかに保存します

ストアと後続のロードの両方がstd::memory_order_seq_cstの場合、これら2つのスレッド間の関係は同じです。違いを確認するには、より多くのスレッドが必要です。

例えばstd::atomic<int>変数xおよびy、どちらも最初は0。

スレッド1:

x.store(1,std::memory_order_release);

スレッド2:

y.store(1,std::memory_order_release);

スレッド3:

int a=x.load(std::memory_order_acquire); // x before y
int b=y.load(std::memory_order_acquire); 

スレッド4:

int c=y.load(std::memory_order_acquire); // y before x
int d=x.load(std::memory_order_acquire);

記述されているように、ストアとxおよびyの間に関係はないため、スレッド3ではa==1b==0、スレッド4ではc==1およびd==0を確認できます。

すべてのメモリ順序がstd::memory_order_seq_cstに変更された場合、これにより、ストア間の順序がxyに強制されます。したがって、スレッド3がa==1b==0を検出した場合、それはxへのストアがyへのストアの前にある必要があることを意味します。したがって、スレッド4がc==1を認識した場合、つまりyへのストアが完了した場合、xへのストアもd==1が必要です。

実際には、どこでもstd::memory_order_seq_cstを使用すると、コンパイラとプロセッサのアーキテクチャに応じて、ロードまたはストア、あるいはその両方にオーバーヘッドが追加されます。例えばx86プロセッサの一般的な手法は、必要な順序の保証を提供するために、std::memory_order_seq_cstストアにXCHG命令ではなく、MOV命令を使用することですが、std::memory_order_releaseには、プレーンなMOVで十分です。よりリラックスしたメモリアーキテクチャを備えたシステムでは、プレーンなロードとストアの保証が少ないため、オーバーヘッドが大きくなる可能性があります。

メモリオーダリングは難しいです。 私の本 で、ほぼすべての章をそれに捧げました。

70

メモリの順序付けは非常に難しい場合があり、間違った場合の影響は非常に微妙なことがよくあります。

すべてのメモリオーダリングの重要なポイントは、何が起こるかではなく、「何が起こったか」を保証することです。たとえば、いくつかの変数(たとえば、x = 7; y = 11;)に何かを格納する場合、別のプロセッサは、xに値7を表示する前に、yを11として表示できる可能性があります。設定xと設定yの間でメモリオーダリング操作を使用することにより、使用しているプロセッサは、yに何かを格納し続ける前に、x = 7;がメモリに書き込まれたことを保証します。

ほとんどの場合、値が最終的に更新される限り、書き込みがどの順序で行われるかはそれほど重要ではありません。しかし、たとえば、整数の循環バッファがあり、次のようなことを行う場合。

buffer[index] = 32;
index = (index + 1)  % buffersize; 

他のスレッドはindexを使用して新しい値が書き込まれたことを確認しているため、最初に32を書き込んでから、indexを更新する必要があります。そうしないと、他のスレッドがoldデータを取得する可能性があります。

セマフォやミューテックスなどを機能させる場合にも同じことが当てはまります。これが、メモリバリアタイプにリリースと取得という用語が使用される理由です。

現在、cstは最も厳密な順序付けルールです。これにより、プロセッサがさらに操作を続行する前に、書き込んだデータの読み取りと書き込みの両方がメモリに出力されます。これは、特定の取得または解放の障壁を実行するよりも遅くなります。これにより、プロセッサは、ストアだけまたはロードだけではなく、ストアとロードが完了したことを確認するように強制されます。

それはどのくらいの違いがありますか?これは、システムアーキテクチャが何であるかに大きく依存します。一部のシステムでは、キャッシュを[部分的に]フラッシュする必要があり、あるコアから別のコアに割り込みを送信して、「続行する前にこのキャッシュフラッシュ作業を実行してください」と言います。これには数百サイクルかかる場合があります。他のプロセッサでは、通常のメモリ書き込みよりもわずかな割合で遅くなります。 X86は、これを高速に実行するのに非常に優れています。一部のタイプの組み込みプロセッサ(たとえば、ARMの一部のモデル-わからない?)は、すべてが機能することを保証するために、プロセッサでもう少し作業が必要です。

9
Mats Petersson