C++ 11標準では、メモリ順序を含むメモリモデル(1.7、1.10)が定義されています。これは、大まかに「逐次一貫性」、「取得」、「消費」、「解放」、と「リラックス」。同様に大まかに言えば、プログラムはレースフリーの場合にのみ正しいです。これは、すべてのアクションを、あるアクションhappens-before別のアクションの順序に並べることができる場合に発生します。アクションXが発生する前アクション[〜#〜] y [〜#〜]の方法は、[〜#〜] x [ 〜#〜]は[〜#〜] y [〜#〜](1つのスレッド内)の前にシーケンスされます、またはX inter-thread-happens-before Y =。後者の条件は、とりわけ、
Synchronizing-withは、[〜#〜] x [〜#〜]がアトミック変数の「リリース」順序を持つアトミックストアであり、[ 〜#〜] y [〜#〜]は、同じ変数で「取得」順序付けされたアトミックロードです。 dependency-ordered-beforeであるということは、[〜#〜] y [〜#〜]が「消費」順序(および適切なメモリアクセス)でロードされる類似の状況で発生します。 )。 synchronizes-withの概念は、スレッド内で相互にsequenced-beforeであるが、-であるアクション間で推移的にhappens-before関係を拡張します。 dependency-ordered-beforeは、sequenced-beforeと呼ばれるcarries-dependencyの厳密なサブセットを介してのみ推移的に拡張されます。これは、一連の大きなルールに従います。特に、std::kill_dependency
で中断できます。
では、「依存関係の順序付け」の概念の目的は何ですか?単純なsequenced-before/synchronizes-withの順序に比べてどのような利点がありますか?ルールが厳しいので、もっと効率的に実装できると思います。
リリース/取得からリリース/消費への切り替えが正しく、重要な利点を提供するプログラムの例を挙げていただけますか?そして、std::kill_dependency
はいつ改善を提供しますか?高レベルの議論は素晴らしいでしょうが、ハードウェア固有の違いに対するボーナスポイントです。
データ依存関係の順序付けは N2492 によって導入され、次の理由があります。
現在の作業ドラフト(N2461)が、一部の既存のハードウェアで可能なスケーラビリティに近いスケーラビリティをサポートしていない2つの重要なユースケースがあります。
- めったに書き込まれない同時データ構造への読み取りアクセス
オペレーティングシステムカーネルとサーバースタイルのアプリケーションの両方で、めったに書き込まれない同時データ構造は非常に一般的です。例には、外部状態(ルーティングテーブルなど)、ソフトウェア構成(現在ロードされているモジュール)、ハードウェア構成(現在使用中のストレージデバイス)、およびセキュリティポリシー(アクセス制御権限、ファイアウォールルール)を表すデータ構造が含まれます。読み取りと書き込みの比率が10億対1をはるかに超えることは非常に一般的です。
- ポインターを介したパブリケーションのパブリッシュ/サブスクライブセマンティクス
スレッド間の通信の多くはポインターを介したものであり、プロデューサーは、コンシューマーが情報にアクセスできるポインターを公開します。そのデータへのアクセスは、完全な取得セマンティクスなしで可能です。
このような場合、スレッド間のデータ依存性の順序付けを使用すると、桁違いの高速化と、相互をサポートするマシンのスケーラビリティが同様に向上します。 -スレッドデータ依存性の順序。このようなマシンは、他の方法で必要とされる高価なロック取得、アトミック命令、またはメモリフェンスを回避できるため、このような高速化が可能です。
強調鉱山
そこに提示されている動機付けのユースケースは、Linuxカーネルからのrcu_dereference()
です。
Load-consumeは、load-conquireにデータに依存する式の評価に対してのみ発生前の関係を誘発することを除いて、load-acquireによく似ています。式をkill_dependency
でラップすると、load-consumeからの依存関係を持たなくなった値になります。
重要なユースケースは、ライターがデータ構造を順番に構築し、共有ポインターを新しい構造にスイングすることです(release
またはacq_rel
アトミックを使用)。リーダーはload-consumeを使用してポインターを読み取り、それを逆参照してデータ構造に到達します。逆参照によってデータの依存関係が作成されるため、リーダーは初期化されたデータを確認できます。
std::atomic<int *> foo {nullptr};
std::atomic<int> bar;
void thread1()
{
bar = 7;
int * x = new int {51};
foo.store(x, std::memory_order_release);
}
void thread2()
{
int *y = foo.load(std::memory_order_consume)
if (y)
{
assert(*y == 51); //succeeds
// assert(bar == 7); //undefined behavior - could race with the store to bar
// assert(kill_dependency(*y) + bar == 58) // undefined behavior (same reason)
assert(*y + bar == 58); // succeeds - evaluation of bar pulled into the dependency
}
}
負荷消費を提供する理由は2つあります。主な理由は、ARMであり、電力負荷は消費することが保証されていますが、それらを取得に変換するには追加のフェンシングが必要です(x86では、すべての負荷が取得であるため、消費によって直接的なパフォーマンス上の利点はありません)ナイーブなコンパイル。)2番目の理由は、コンパイラがデータに依存せずに後の操作を消費前まで移動できることです。これは、取得では実行できません(このような最適化を有効にすることが、このメモリオーダリングをすべてに組み込む大きな理由です。言語。)
値をkill_dependency
でラップすると、ロード消費の前に移動する値に依存する式を計算できます。これは便利です。値が以前に読み取られた配列へのインデックスである場合。
消費を使用すると、推移的ではなくなった発生前の関係が発生することに注意してください(ただし、非循環であることが保証されています)。たとえば、bar
へのストアは、fooへのストアの前に発生します。これは、y
の逆参照の前に発生します。これは、(コメントアウトされた)bar
の読み取りの前に発生します。アサート)が、bar
へのストアはbar
の読み取りの前には発生しません。これにより、happens-beforeの定義がかなり複雑になりますが、それがどのように機能するかを想像できます(sequenced-beforeから始めて、release-consume-dataDependencyまたはrelease-acquire-sequencedBeforeリンクをいくつでも伝播します)。
Jeff Preshingには、この質問に答えるすばらしいブログ投稿があります。自分で何も追加することはできませんが、消費するか取得するかについて疑問に思っている人は、彼の投稿を読む必要があると思います。
http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
彼は、3つの異なるアーキテクチャにわたる対応するベンチマークアセンブリコードを含む特定のC++の例を示しています。に比べ memory_order_acquire
、memory_order_consume
は、PowerPCで3倍のスピードアップ、ARMで1.6倍のスピードアップ、そしてx86で無視できるほどのスピードアップを提供する可能性があります。キャッチは、彼がそれを書いた時点で、実際に処理されたGCCのみが、取得とは異なる方法で消費セマンティクスを処理したことです。おそらくバグが原因です。それにもかかわらず、コンパイラの作成者がそれを利用する方法を理解できれば、スピードアップが利用可能であることを示しています。
それは本当の答えではなく、適切な答えに大きな報奨金がないという意味ではありませんが、部分的な発見を記録したいと思います。
1.10をしばらく見つめた後、特に段落11の非常に役立つメモを見て、これは実際にはそれほど難しいことではないと思います。 synchronizes-with(以下:s/w)とdependency-ordered-before(dob)の大きな違いは、happens-beforesequenced-before(s/b)とs/wを任意に連結することで関係を確立できますが、dobの場合はそうではありませんです。 スレッド間が発生する前に発生するの定義の1つに注意してください。
A
はX
と同期し、X
はB
の前にシーケンスされます
しかし、の類似のステートメント 不足している!A
は依存関係です-X
の前に順序付けられます
したがって、リリース/取得(つまり、s/w)を使用すると、任意のイベントを注文できます。
A1 s/b B1 Thread 1
s/w
C1 s/b D1 Thread 2
しかし、ここで、次のようなイベントの任意のシーケンスについて考えてみます。
A2 s/b B2 Thread 1
dob
C2 s/b D2 Thread 2
このシーケンスでは、A2
happens-beforeC2
(A2
はs/bB2
およびB2
スレッド間は前に発生するC2
であるため、それは依然として真実です。私たちはあなたが実際に言うことは決してできないと主張することができます!)。ただし、正しくありませんそのA2
happens-beforeD2
。イベントA2
とD2
は相互に順序付けられていません、ただし実際にはC2
依存関係を運ぶD2
を保持します。これはより厳しい要件であり、その要件がない場合、リリース/消費ペアの「全体」でA2
- to -D2
を注文することはできません。
言い換えると、リリース/消費のペアは、依存関係を持つアクションの順序のみを伝播します。依存しないものはすべて、リリース/消費のペア全体で順序付けられるわけではありません。
さらに、最後のより強力なリリース/取得ペアを追加すると、順序が復元されることに注意してください。
A2 s/b B2 Th 1
dob
C2 s/b D2 Th 2
s/w
E2 s/b F2 Th 3
引用されたルールにより、D2
スレッド間は前に発生しますF2
、したがってC2
とB2
も同様であり、A2
happens-beforeF2
。ただし、A2
とD2
の間にはまだ順序がないことに注意してください。順序はA2
と後でイベントの間だけです。
要約すると、最後に、依存関係の伝達は一般的な順序付けの厳密なサブセットであり、リリース/消費のペアは、依存関係を伝達するアクション間でのみ順序付けを提供します。より強力な順序付けが必要ない限り(たとえば、リリース/取得ペアを通過することによって)、依存関係チェーン内のnotであるすべてのものは自由に並べ替えることができるため、理論的には追加の最適化の可能性があります。
たぶんここに意味のある例がありますか?
std::atomic<int> foo(0);
int x = 0;
void thread1()
{
x = 51;
foo.store(10, std::memory_order_release);
}
void thread2()
{
if (foo.load(std::memory_order_acquire) == 10)
{
assert(x == 51);
}
}
記述されているように、コードはレースフリーであり、リリース/取得ペアがアサーションのロードの前にストアx = 51
を注文するため、アサーションは保持されます。ただし、「acquire」を「consume」に変更すると、これは正しくなくなり、x = 51
はストアにx
への依存関係を持たないため、プログラムはfoo
でデータ競合を起こします。最適化のポイントは、依存関係がないため、このストアはfoo
が何をしているかに関係なく自由に並べ替えることができるということです。