web-dev-qa-db-ja.com

取得操作の前に並べ替えを防ぐC ++メモリモデルの正確なルールは何ですか?

次のコードの操作の順序について質問があります。

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 == 0thread1を同時に実行した後、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);
}

r1r2を同時に実行した後、thread1thread2の両方を0に等しくすることは可能ですか?そうでない場合、どのC++ルールがこれを防ぎますか?

18
Oleg Andreev

この標準では、特定の順序付けパラメーターを使用してアトミック操作を中心に操作を順序付ける方法に関して、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を使用する必要があります。これにより、関連する操作の単一の合計順序が保証されます。

11
LWimsey

あなたはすでにこれの言語弁護士の部分への答えを持っています。しかし、 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を置きます。 exchangeseq_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の実装に悪影響を及ぼします。

3
Peter Cordes

言語弁護士の推論は理解しにくいので、アトミックを理解しているプログラマーがあなたの質問の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をロードする場合でも、リリースと取得の関係全体は関係ありません。

*)ただし、メモリモデルとの不整合のために特定の値が発生しないことを示すことはさらに理由がありますが、ここでは発生しません。

3
Carlo Wood

元のバージョンでは、ストアが他のスレッドを読み取る前に他のスレッドに伝播する必要がないため、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つはloadsは、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
2
Caleth