次のコードの操作の順序について質問があります。
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_relaxed);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_relaxed);
}
Cppreferenceページ( https://en.cppreference.com/w/cpp/atomic/memory_order )のstd::memory_order_acquire
の説明を考えると、
このメモリ順序でのロード操作は、影響を受けるメモリ位置で取得操作を実行します。このロードの前に、現在のスレッドでの読み取りまたは書き込みを並べ替えることはできません。
r1 == 0 && r2 == 0
とthread1
を同時に実行した後、thread2
という結果が発生することは決してないことは明らかです。
ただし、C++標準(現在C++ 14ドラフトを参照)には文言が見つかりません。これにより、2つの緩和されたロードを取得-リリース交換で並べ替えることができないことが保証されます。何が足りないのですか?
編集:コメントで示唆されているように、r1とr2の両方をゼロに等しくすることは実際に可能です。次のようにload-acquireを使用するようにプログラムを更新しました。
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_acquire);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_acquire);
}
r1
とr2
を同時に実行した後、thread1
とthread2
の両方を0に等しくすることは可能ですか?そうでない場合、どのC++ルールがこれを防ぎますか?
この標準では、特定の順序付けパラメーターを使用してアトミック操作を中心に操作を順序付ける方法に関して、C++メモリモデルを定義していません。代わりに、取得/解放順序付けモデルの場合、スレッド間でデータを同期する方法を指定する「同期する」や「発生する前」などの正式な関係を定義します。
N4762、§29.4.2-[atomics.order]
アトミックオブジェクトMに対してリリース操作を実行するアトミック操作Aは、Mに対して取得操作を実行するアトミック操作Bと同期し、Aが先頭にあるリリースシーケンスの任意の副作用からその値を取得します。
§6.8.2.1-9では、ストアAがロードBと同期する場合、Aの前にシーケンスされたものはすべてスレッド間でBの後にシーケンスされたものが「発生する」と規定されています。
2番目の例(最初の例はさらに弱い)では、実行時の関係(負荷からの戻り値をチェックする)が欠落しているため、「同期」(したがってスレッド間が発生する前)の関係は確立されません。
ただし、戻り値を確認したとしても、exchange
操作は実際には何も「解放」しない(つまり、これらの操作の前にメモリ操作がシーケンスされない)ため、役に立ちません。 Neiterは、ロード後に操作がシーケンスされないため、アトミックロード操作を「取得」します。
したがって、標準によれば、両方の例(0 0を含む)の負荷に対して考えられる4つの結果のそれぞれが有効です。実際、標準によって与えられる保証は、すべての操作でmemory_order_relaxed
よりも強力ではありません。
コードで00の結果を除外する場合は、4つの操作すべてでstd::memory_order_seq_cst
を使用する必要があります。これにより、関連する操作の単一の合計順序が保証されます。
あなたはすでにこれの言語弁護士の部分への答えを持っています。しかし、 RMWアトミックのLL/SC を使用する可能性のあるCPUアーキテクチャでasmでこれが可能である理由を理解する方法の関連する質問に答えたいと思います。
C++ 11がこの並べ替えを禁止することは意味がありません。この場合、一部のCPUアーキテクチャでストアロードバリアを回避できるため、ストアロードバリアが必要になります。
C++ 11のメモリオーダーをasm命令にマップする方法を考えると、PowerPC上の実際のコンパイラで実際に可能かもしれません。
PowerPC64では、acq_rel交換と取得ロード(静的変数の代わりにポインター引数を使用)を持つ関数は、gcc6.3 -O3 -mregnames
で次のようにコンパイルされます。これはC11バージョンのものです。MIPSとSPARCのclang出力を確認したかったので、GodboltのclangセットアップはC11 <atomic.h>
で機能しますが、<atomic>
を使用するとC++ 11-target sparc64
では失敗します。
(ソース+ asm MIPS32R6、SPARC64、ARM 32、およびPowerPC64のGodbolt) )
foo:
lwsync # with seq_cst exchange this is full sync, not just lwsync
# gone if we use exchage with mo_acquire or relaxed
# so this barrier is providing release-store ordering
li %r9,1
.L2:
lwarx %r10,0,%r4 # load-linked from 0(%r4)
stwcx. %r9,0,%r4 # store-conditional 0(%r4)
bne %cr0,.L2 # retry if SC failed
isync # missing if we use exchange(1, mo_release) or relaxed
ld %r3,0(%r3) # 64-bit load double-Word of *a
cmpw %cr7,%r3,%r3
bne- %cr7,$+4 # skip over the isync if something about the load? PowerPC is weird
isync # make the *a load a load-acquire
blr
isync
はストアロードバリアではありません。ローカルで完了するには、前述の手順のみが必要です(コアのアウトオブオーダー部分からリタイアします)。他のスレッドが以前のストアを見ることができるように、ストアバッファがフラッシュされるのを待ちません。
したがって、交換の一部であるSC(stwcx.
)ストアは、ストアバッファーに配置され、グローバルに表示されるようになりますafter純粋それに続くacquire-load。実際、別のQ&Aがすでにこれを尋ねており、その答えは、この並べ替えが可能であると考えているということです。 `isync`はStore-Loadの並べ替えを防ぎますか? CPU PowerPCで?
純粋な負荷がseq_cst
の場合、PowerPC64gccはsync
の前にld
を置きます。 exchange
seq_cst
を作成するとnot並べ替えが防止されます。 C++ 11は、SC操作の合計順序を1つだけ保証するため、C++ 11の場合は交換とロードの両方がSC)である必要があることに注意してください。それを保証するために。
そのため、PowerPCには、C++ 11からアトミック用のasmへの少し変わったマッピングがあります。ほとんどのシステムでは、店舗に重いバリアを設置しているため、seq-cstの負荷を安くするか、片側にのみバリアを設けることができます。これがPowerPCの有名な弱いメモリオーダリングに必要だったのか、それとも別の選択が可能だったのかはわかりません。
https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html は、さまざまなアーキテクチャで可能な実装を示しています。 ARMの複数の選択肢について言及しています。
AArch64では、thread1の元のC++バージョンでこれを取得します。
thread1():
adrp x0, .LANCHOR0
mov w1, 1
add x0, x0, :lo12:.LANCHOR0
.L2:
ldaxr w2, [x0] @ load-linked with acquire semantics
stlxr w3, w1, [x0] @ store-conditional with sc-release semantics
cbnz w3, .L2 @ retry until exchange succeeds
add x1, x0, 8 @ the compiler noticed the variables were next to each other
ldar w1, [x1] @ load-acquire
str w1, [x0, 12] @ r1 = load result
ret
AArch64リリースストアは順次-リリースであり、プレーンリリースではないため、そこで並べ替えを行うことはできません。これは、後のロードで並べ替えることができないことを意味します。
しかし、プレーンリリースのLL/SCアトミックもある、または代わりに持っている架空のマシンでは、acq_relが、後で異なるキャッシュラインへのロードがグローバルに表示されるのを停止しないことは簡単にわかります。 LL、ただし交換のSC)の前。
exchange
がx86のように単一のトランザクションで実装されているため、ロードとストアがメモリ操作のグローバル順序で隣接している場合、それ以降の操作をacq_rel
交換で並べ替えることはできず、基本的にseq_cst
と同等です。 。
ただし、RMWアトミック性を与えるためにLL/SCが真のアトミックトランザクションである必要はありませんその場所。
実際、単一のasm swap
命令は、リラックスしたセマンティクスまたはacq_relセマンティクスを持っている可能性があります。 SPARC64はmembar
命令の周りにswap
命令を必要とするため、x86のxchg
とは異なり、それ自体はseq-cstではありません。 (SPARCには、特にPowerPCと比較して、非常に優れた、人間が読める形式の命令ニーモニックがあります。基本的に、PowerPCよりも読みやすいものは何でもあります。)
したがって、C++ 11が要求することは意味がありません。それは、他の方法ではストア負荷バリアを必要としないCPUの実装に悪影響を及ぼします。
言語弁護士の推論は理解しにくいので、アトミックを理解しているプログラマーがあなたの質問の2番目のスニペットについて推論する方法を追加したいと思いました。
これは対称的なコードなので、片側だけを見るだけで十分です。質問はr1(r2)の値に関するものなので、まずは
r1 = x.load(std::memory_order_acquire);
R1の値に応じて、他の値の可視性について何かを言うことができます。ただし、r1の値はテストされていないため、取得は関係ありません。いずれの場合も、r1の値は、これまでに書き込まれた任意の値にすることができます(過去または将来*))。したがって、ゼロにすることができます。それでも、プログラム全体の結果が0 0になるかどうかに関心があるため、ゼロであると見なすことができます。これは、r1の値をテストする一種です。
したがって、ゼロを読み取ったとすると、そのゼロがmemory_order_releaseを使用して別のスレッドによって書き込まれた場合、ストアリリースの前にそのスレッドによって行われたメモリへの他のすべての書き込みもこのスレッドに表示されます。ただし、読み取ったゼロの値はxの初期化値であり、初期化値は非アトミックであり、「リリース」は言うまでもなく、その値を書き込むという点で、それらの前に「順序付け」されたものはありませんでした。メモリへ;したがって、他のメモリ位置の可視性については何も言えません。言い換えると、「取得」は無関係です。
したがって、r1 = 0を取得でき、acquireを使用したという事実は関係ありません。同じ理由がr2にも当てはまります。したがって、結果はr1 = r2 = 0になります。
実際、ロード取得後にr1の値が1であり、その1がメモリオーダリングリリースを使用してthread2によって書き込まれたと仮定すると(これは、1の値が書き込まれる唯一の場所であるためです。 x)次に、thread2 beforeによってメモリに書き込まれたすべてのものがthread1にも表示されることがわかっています(provided thread1 read x == 1したがって!)。ただし、thread2はxに書き込む前に何も書き込まないため、値1をロードする場合でも、リリースと取得の関係全体は関係ありません。
*)ただし、メモリモデルとの不整合のために特定の値が発生しないことを示すことはさらに理由がありますが、ここでは発生しません。
元のバージョンでは、ストアが他のスレッドを読み取る前に他のスレッドに伝播する必要がないため、r1 == 0 && r2 == 0
を表示できます。これは、どちらのスレッドの操作の並べ替えではありませんが、たとえば、古いキャッシュの読み取り。
Thread 1's cache | Thread 2's cache
x == 0; | x == 0;
y == 0; | y == 0;
y.exchange(1, std::memory_order_acq_rel); // Thread 1
x.exchange(1, std::memory_order_acq_rel); // Thread 2
スレッド1でのリリースは、スレッド2によって無視され、その逆も同様です。抽象マシンでは、スレッドのx
およびy
の値との整合性がありません
Thread 1's cache | Thread 2's cache
x == 0; // stale | x == 1;
y == 1; | y == 0; // stale
r1 = x.load(std::memory_order_relaxed); // Thread 1
r2 = y.load(std::memory_order_relaxed); // Thread 2
通常の順序付けルールと「目に見える副作用になる」ルールを組み合わせて、取得/解放ペアで「因果関係の違反」を取得するには、moreスレッドが必要です。少なくとも1つはload
sは、1
を表示します。
一般性を失うことなく、スレッド1が最初に実行されると仮定しましょう。
Thread 1's cache | Thread 2's cache
x == 0; | x == 0;
y == 0; | y == 0;
y.exchange(1, std::memory_order_acq_rel); // Thread 1
Thread 1's cache | Thread 2's cache
x == 0; | x == 0;
y == 1; | y == 1; // sync
スレッド1のリリースは、スレッド2の取得とペアを形成し、抽象マシンは、両方のスレッドで一貫したy
を記述します。
r1 = x.load(std::memory_order_relaxed); // Thread 1
x.exchange(1, std::memory_order_acq_rel); // Thread 2
r2 = y.load(std::memory_order_relaxed); // Thread 2