C ++ 11でStoreLoadバリアを実現する方法は?


13

古典的な問題の変形を解決する移植可能なコード(Intel、ARM、PowerPC ...)を記述したいと思います。

Initially: X=Y=0

Thread A:
  X=1
  if(!Y){ do something }
Thread B:
  Y=1
  if(!X){ do something }

ここでの目標は、両方のスレッドがやっているような状況を避けるためですsomething。(どちらも実行しなくても問題ありません。これは1回だけ実行するメカニズムではありません。)以下の私の推論に欠陥がある場合は、修正してください。

私は次のようにmemory_order_seq_cstアトミックstoresおよびloads を使用して目標を達成できることを認識しています。

std::atomic<int> x{0},y{0};
void thread_a(){
  x.store(1);
  if(!y.load()) foo();
}
void thread_b(){
  y.store(1);
  if(!x.load()) bar();
}


{x.store(1), y.store(1), y.load(), x.load()}イベントにはいくつかの単一の合計順序が必要であり、プログラムの順序「エッジ」に同意する必要があるため、これは目標を達成します。

  • x.store(1) 「TOは前に」 y.load()
  • y.store(1) 「TOは前に」 x.load()

foo()呼び出された場合、追加のエッジがあります:

  • y.load() 「前に値を読み取る」 y.store(1)

bar()呼び出された場合、追加のエッジがあります:

  • x.load() 「前に値を読み取る」 x.store(1)

そして、これらすべてのエッジを結合すると、サイクルが形成されます。

x.store(1)「TOが前にある」y.load()「前に値を読み取る」y.store(1)「TOが前にある」x.load()「前に値を読み取る」x.store(true)

これは、注文に循環がないという事実に違反しています。

happens-beforeこれらのエッジが実際にhappens-before関係を暗示するという私の仮定の正確さについてフィードバックを求めたいので、私はのような標準的な用語とは対照的に、非標準的な用語「TOが前にある」と「前に値を読み取る」を意図的に使用しています。グラフ、およびそのような結合されたグラフのサイクルは禁止されています。それについてはよくわかりません。私が知っているのは、このコードがIntel gccとclangおよびARM gccで正しいバリアを生成することです


ここで、「X」を制御できないため、実際の問題はもう少し複雑です。マクロ、テンプレートなどの背後に隠れており、 seq_cst

"X"が単一の変数なのか、それとも他の概念(たとえば、軽量のセマフォやミューテックス)なのかさえわかりません。私が知っているのは、2つのマクロがset()あり、別のスレッドがを呼び出した後に「戻る」check()というマクロをcheck()返すことだけです。(それはされてもいることを知らおよびスレッドセーフであり、データ・レースUBを作成することはできません。)trueset()setcheck

したがって、概念的にset()は「X = 1」やcheck()「X」に似ていますが、アトミックに直接アクセスすることはできません。

void thread_a(){
  set();
  if(!y.load()) foo();
}
void thread_b(){
  y.store(1);
  if(!check()) bar();
}

私は、心配しているset()内部として実装されるかもしれないx.store(1,std::memory_order_release)および/またはcheck()かもしれませんx.load(std::memory_order_acquire)。または、仮にstd::mutex1つのスレッドがロックを解除し、別のスレッドがtry_lockingしているということです。ISO標準でstd::mutexは、seq_cstではなく、取得と解放の順序が保証されています。

これが当てはまる場合は、check()前にボディを「並べ替え」できるかどうかですy.store(true)PowerPCでこれが発生することを示すAlexの回答を参照してください)。
これでこの一連のイベントが可能になるため、これは本当に悪いことです。

  • thread_b()最初にx0)の古い値をロードします
  • thread_a() を含むすべてを実行します foo()
  • thread_b() を含むすべてを実行します bar()

それで、両方foo()bar()も呼ばれました、私はそれを避けなければなりませんでした。それを防ぐための私の選択肢は何ですか?


オプションA

Store-Loadバリアを強制してみてください。これは、実際には次の方法で実現できます。Alexが別の回答でstd::atomic_thread_fence(std::memory_order_seq_cst);説明したように、テストされたすべてのコンパイラは完全なフェンスを放出しました。

  • x86_64:MFENCE
  • PowerPC:hwsync
  • イタヌイム:mf
  • ARMv7 / ARMv8:dmb ish
  • MIPS64:同期

このアプローチの問題は、C ++ルールで保証を見つけることができずstd::atomic_thread_fence(std::memory_order_seq_cst)、完全なメモリバリアに変換する必要があることです。実際、atomic_thread_fenceC ++のsの概念は、メモリバリアのアセンブリの概念とは異なる抽象化レベルにあり、「アトミック操作が何と同期するか」のようなものを扱っています。以下の実装が目標を達成したという理論的な証拠はありますか?

void thread_a(){
  set();
  std::atomic_thread_fence(std::memory_order_seq_cst)
  if(!y.load()) foo();
}
void thread_b(){
  y.store(true);
  std::atomic_thread_fence(std::memory_order_seq_cst)
  if(!check()) bar();
}

オプションB

Y上で読み取り-変更-書き込みのmemory_order_acq_rel操作を使用して、同期を達成するためにYを介して持っている制御を使用します。

void thread_a(){
  set();
  if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
  y.exchange(1,std::memory_order_acq_rel);
  if(!check()) bar();
}

ここでの考え方は、単一のアトミック(y)へのアクセスは、すべてのオブザーバーが同意する単一の順序を形成する必要があるということです。つまり、fetch_addexchangeか逆のどちらかです。

fetch_add前にあるexchange場合、の「解放」部分はfetch_addの「取得」部分と同期します。exchangeしたがって、のすべての副作用がset()コード実行から見えるようにするcheck()必要bar()があるため、呼び出されません。

それ以外の場合は、exchange前であるfetch_add、その後、fetch_add表示されます1と呼んでいませんfoo()。だから、両方を呼び出すことは不可能であるfoo()bar()。この推論は正しいですか?


オプションC

ダミーアトミックを使用して、災害を防止する「エッジ」を導入します。次のアプローチを検討してください。

void thread_a(){
  std::atomic<int> dummy1{};
  set();
  dummy1.store(13);
  if(!y.load()) foo();
}
void thread_b(){
  std::atomic<int> dummy2{};
  y.store(1);
  dummy2.load();
  if(!check()) bar();
}

ここでatomicの問題がローカルであると思われる場合は、それらをグローバルスコープに移動することを想像してください。次の理由から、それは私には重要ではないように思われます。ダミー1がいかに面白いかを明らかにするような方法でコードを意図的に記述しました。とdummy2は完全に独立しています。

なぜこれがうまくいくのでしょうか?さて、{dummy1.store(13), y.load(), y.store(1), dummy2.load()}プログラムの順序「エッジ」と一致する必要があるいくつかの単一の合計順序が存在する必要があります。

  • dummy1.store(13) 「TOは前に」 y.load()
  • y.store(1) 「TOは前に」 dummy2.load()

(seq_cstストア+ロードは、個別のバリア命令が必要ないAArch64を含む実際のISAでasmで実行されるように、StoreLoadを含む完全なメモリバリアと同等のC ++を形成することが期待されます。)

ここで、考慮すべき2つのケースがあります。それは、全体の順序のy.store(1)y.load()または後です。

場合y.store(1)の前でy.load()、その後foo()と呼ばれ、私たちは安全であることはありません。

もし y.load()がbeforeのy.store(1)、それをプログラムの順序ですでに持っている2つのエッジと組み合わせて、それを推定します。

  • dummy1.store(13) 「TOは前に」 dummy2.load()

さて、dummy1.store(13)の効果を解除する解除操作は、ありset()、そしてdummy2.load()ので、取得操作でcheck()の効果が表示されるはずですset()ため、bar()呼び出されませんし、私たちは安全です。

check()の結果が表示されると思うのは、ここで正しいset()ですか。さまざまな種類の「エッジ」(「プログラム順序」または「シーケンス前」、「合計順序」、「リリース前」、「取得後」)をそのように組み合わせることができますか?私はこれについて深刻な疑問を抱いています:C ++ルールは、同じ場所でのストアとロードの間の「同期」の関係について話しているようです-ここにはそのような状況はありません。

seq_cst全体の順序で前にあることdumm1.storeわかっている場合(他の理由により)についてのみ心配していることに注意してくださいdummy2.load。したがって、それらが同じ変数にアクセスしていた場合、ロードは格納された値を確認し、それと同期していました。

(アトミックなロードとストアが少なくとも1方向のメモリバリアにコンパイルされる実装のメモリバリア/順序変更の推論(およびseq_cst操作は順序変更できない:たとえばseq_cstストアはseq_cstロードを渡すことができない)は、任意のロード/店は後にdummy2.load間違いなく他のスレッドに見えるようになり後に y.store。そして、同様に他のスレッドのため、...の前にy.load。)


https://godbolt.org/z/u3dTa8でオプションA、B、Cの実装を試すことができます。


1
C ++メモリモデルには、StoreLoadの並べ替えの概念はありません。(そして、実際のハードウェアのasmとは異なり、非アトミックオブジェクトのデータレースでのUB。)私が知っているすべての実際の実装では std::atomic_thread_fence(std::memory_order_seq_cst)、完全なバリアにコンパイルされますが、全体の概念は実装の詳細なので、見つけられません標準でのそれの言及。(CPU・メモリ・モデルは、通常れる reoreringsシーケンシャル一貫に対して許可されるものに関して定義された、例えばx86のある配列-CST +ストアバッファW /転送)
ピーターコルド

@PeterCordesのおかげで、私は書面で不明確だったかもしれません。「オプションA」で書いた内容をお伝えしたいと思います。質問のタイトルに「StoreLoad」という単語が使用されていることは知っています。「StoreLoad」はまったく異なる世界のコンセプトです。私の問題は、この概念をC ++にマッピングする方法です。または、直接マッピングできない場合は、私が提示した目標を達成する方法:両方が呼び出されないようにすることfoo()bar()、呼び出されないようにすること。
qbolec

1
を使用compare_exchange_*して、その値を変更せずにアトミックブールに対してRMW操作を実行できます(単純に期待値と新規値を同じ値に設定します)。
mpoeter

1
@Fareanorとqbolec:atomic<bool>ありexchangecompare_exchange_weak。後者は、CAS(true、true)またはfalse、falseによって(試行する)ダミーのRMWを実行するために使用できます。失敗するか、または値を自動的にそれ自体で置き換えます。(x86-64 asmでは、そのトリックlock cmpxchg16bは保証されたアトミックな16バイトのロードを行う方法です。非効率的ですが、個別のロックを取得するよりも悪くはありません。)
Peter Cordes

1
@PeterCordesはい、どちらfoo()bar()呼び出されないことも起こり得ます。「問題Xがあると思うが、問題Yがある」という種類の応答を回避するために、コードの多くの「現実の世界」の要素を取り入れたくありませんでした。しかし、人は本当に背景階建てであるかを知る必要がある場合:set()本当にされsome_mutex_exit()check()されtry_enter_some_mutex()y「いくつかのウェイターがあります」と、foo()「誰にも目覚めないで終了」で、bar()「wakup待ち」である...しかし、私がすることを拒否このデザインについてここで議論してください-私はそれを本当に変えることはできません。
qbolec

回答:


5

オプションAとBは有効なソリューションです。

  • オプションA:seq-cstフェンスが何に変換されるかは実際には問題ではありません。C++標準では、提供される保証が明確に定義されています。私はそれらをこの投稿でレイアウトしました:memory_order_seq_cstフェンスはいつ役に立ちますか?
  • オプションB:はい、あなたの推論は正しいです。一部のオブジェクトのすべての変更には単一の合計順序(変更順序)があるため、それを使用してスレッドを同期し、すべての副作用の可視性を確保できます。

しかし、選択肢Cがあるではない有効な!synchronize-with関係は、同じオブジェクトの取得/解放操作によってのみ確立できます。あなたの場合、完全に異なる2つの独立したオブジェクトdummy1とがありdummy2ます。ただし、これらを使用して、前に発生する関係を確立することはできません。実際、アトミック変数は純粋にローカルであるため(つまり、アトミック変数は1つのスレッドによってのみ操作されるため)、コンパイラーはas-ifルールに基づいてそれらを自由に削除できます

更新

オプションA:
あるアトミック値 を想定set()check()て操作します。次に、次のような状況になります(->はシーケンス前を示します)。

  • set()-> fence1(seq_cst)->y.load()
  • y.store(true)-> fence2(seq_cst)->check()

したがって、次のルールを適用できます。

アトミック操作のためのA及びB原子オブジェクトにMAが変更MBがある場合、その値をとりmemory_order_seq_cstフェンスX及びYがするようAが前に配列決定され、XはYが前に配列決定されているB、およびXは前にYSを、次に、BAの影響、またはその後のMの変更のいずれかを変更順序で観察します。

すなわち、いずれかcheck()に格納された値を見てset、またはy.load()値書き込むことを見てy.store()(操作は上でyも使用することができますmemory_order_relaxed)。

オプションC:C ++ 17の標準状態[32.4.3、P1347]:

すべての操作で単一の合計注文Sがありmemory_order_seq_cst、影響を受けるすべての場所の「前に発生した」注文と変更注文と一致します[...]

ここで重要な言葉は「一貫性」です。これは、動作場合ことを意味Aが発生し、前の操作B、次いでAが先行しなければならないBの中でS。しかし、我々は逆を推測することはできませんので、論理的含意は、一方通行-通りです:いくつかの操作だけであるためCは、操作の前にDSは、ことを意味するものではありませんCは前に起こっD

特に、2つの個別のオブジェクトに対する2つのseq-cst操作は、Sで操作が完全に順序付けられている場合でも、発生前の関係を確立するために使用できません。個別のオブジェクトに対する操作を順序付ける場合は、seq-cstを参照する必要があります。 -fences(オプションAを参照)。


オプションCが無効であることは明らかではありません。プライベートオブジェクトに対するseq-cst操作でも、他の操作をある程度順序付けることができます。同期するものがないことに同意しましたが、fooとbarのどちらが実行されるか(またはどちらも実行されない)は気にせず、両方が実行されないことを確認しました。シーケンス前の関係とseq-cst操作の合計順序(存在する必要があります)は、私にそれを与えていると思います。
Peter Cordes

@mpoeterありがとうございます。オプションAについて詳しく説明してください。回答の3つの箇条書きのうち、どれがここに当てはまりますか?IIUC y.load()がの効果をy.store(1)認識しない場合、Sではatomic_thread_fence、thread_aがthread_bの前atomic_thread_fenceにあるというルールから証明できます。私が見ないのは、これからset()副作用が見えるという結論に至る方法check()です。
qbolec

1
@qbolec:私は、オプションのA.の詳細と私の答えを更新しました
mpoeter

1
はい、ローカルseq-cst操作は、すべてのseq-cst操作で単一の合計注文Sの一部です。しかし、Sは「専用」であるため、変更注文する前に、起こると一致、すなわち、場合Aが起こる-前にB、そしてAが 先行しなければならないBをしてS。しかし、逆はちょうどので、すなわち、保証するものではありませんAの先行のBSは、我々はない推測することができ、ことをAが起こる-前にB
mpoeter

1
まあ、安全に並列実行できるsetcheckすれば、共有変数の競合を回避できるため、特にパフォーマンスが重要な場合は、おそらくオプションAを使用しますy
mpoeter

1

最初の例では、y.load()0を読み取ることは、それy.load()がbeforeに発生することを意味しませんy.store(1)

ただし、seq_cstロードが合計順序の最後のseq_cstストアの値、または前に発生しなかった非seq_cstストアの値のいずれかを返すというルールのおかげで、単一の合計順序の前にあることを意味しますそれ(この場合は存在しません)。したがって、全体の順序y.store(1)よりも早い場合y.load()y.load() 1が返されます。

単一の合計注文には循環がないため、証明は依然として正しいです。

このソリューションはどうですか?

std::atomic<int> x2{0},y{0};

void thread_a(){
  set();
  x2.store(1);
  if(!y.load()) foo();
}

void thread_b(){
  y.store(1);
  if(!x2.load()) bar();
}

OPの問題は、「X」を制御できないことです。これは、ラッパーマクロなどの背後にあり、seq-cstストア/ロードではない可能性があります。私はそれをよりよく強調するために質問を更新しました。
Peter Cordes

@PeterCordesアイデアは、彼が制御できる別の「x」を作成することでした。わかりやすくするために、回答では「x2」に名前を変更します。私はいくつかの要件を見逃していると確信していますが、唯一の要件がfoo()とbar()の両方が呼び出されないことを確認することである場合、これはそれを満たします。
Tomek Czajka

ですからif(false) foo();、OPもそれを望んでいないと思います:P興味深い点ですが、OPは、条件付き呼び出しが指定した条件に基づいていることを望んでいると思います!
Peter Cordes

1
こんにちは@TomekCzajka、新しいソリューションを提案するために時間を割いていただきありがとうございます。それは私の特定のケースでは機能しません。重要な副作用が省略さcheck()れているためです(の実際の意味については、私の質問に対する私のコメントを参照してくださいset,check,foo,bar)。if(!x2.load()){ if(check())x2.store(0); else bar(); }代わりに使用できると思います。
qbolec

1

@mpoeterは、オプションAとBが安全である理由を説明しました。

実際の実装では、オプションAが必要とするのstd::atomic_thread_fence(std::memory_order_seq_cst)はスレッドA だけであり、Bではないと思います。

実際のseq-cstストアには完全なメモリバリアが含まれます。または、AArch64では、少なくとも取得またはseq_cstロードで再配列できません(stlrシーケンシャルリリースはldar、キャッシュから読み取る前にストアバッファーから排出する必要があります)。

C ++-> asmマッピングでは、アトミックストアまたはアトミックロードでストアバッファーをドレインするコストを選択できます。実際の実装の正しい選択は、アトミックロードを安価にすることです。そのため、seq_cstストアには完全なバリア(StoreLoadを含む)が含まれます。seq_cstのロードは、ほとんどの取得ロードと同じです。

(ただし、POWERではありません。seq_cstはすべてのスレッドが次の順序に同意できる必要があるため、ロードでも同じコア上の他のSMTスレッドからのストア転送を停止するために重い同期=完全なバリアが必要です。すべてのseq_cst ops。 異なるスレッドの異なる場所への2つのアトミックな書き込みは、他のスレッドによって常に同じ順序で見られますか?

(もちろん、安全を正式に保証するために、取得/解放set()-> check()をseq_cst synchronizes-withに昇格させるために両方にフェンスが必要です。リラックスしたセットでも機能すると思いますが、リラックスしたチェックでは、他のスレッドのPOVからbarで並べ替えることができます。)


オプションCの本当の問題は、同期操作が可能な仮想観測者とyダミー操作に依存していることだと思います。 したがって、バリアベースのISAのasmを作成するときに、コンパイラがその順序を保持することを期待しています。

これは、実際のISAでは実際に当てはまります。両方のスレッドには完全なバリアまたは同等の機能が含まれており、コンパイラは(まだ)アトミックを最適化していません。しかしもちろん、「バリアベースのISAへのコンパイル」はISO C ++標準の一部ではありません。 Coherent共有キャッシュは、asm推論には存在するが、ISO C ++推論には存在しない架空のオブザーバーです。

仕事にオプションCについては、我々のような順序必要dummy1.store(13);/ y.load()/ set();いくつかのISO C ++規則に違反する(スレッドBで見られるように)

これらのステートメントを実行するスレッドは、あたかも動作する必要があります set()最初に実行さ(Sequenced Beforeのため)。それは問題ありません。実行時のメモリの順序付け、および/または操作のコンパイル時の順序変更でも、それが可能です。

2つのseq_cst ops d1=13ySequenced Before(プログラムの順序)と一致しています。 set()seq_cstではないため、seq_cst opsの必須のグローバルオーダーには参加しません。

スレッドBはdummy1.storeで、同期しませんので、何が起こる-前に要件にsetに対するd1=13適用され、その割り当てが解除操作であっても。

他に考えられるルール違反はありません。setSequenced-Before との一貫性を保つために必要なものが見つかりませんd1=13

「dummy1.storeがset()を解放する」という推論は欠陥です。その順序付けは、それと同期する、またはasm内の実際のオブザーバーにのみ適用されます。 @mpoeterが答えたように、seq_cstの合計順序の存在は、前に発生する関係を作成したり暗示したりするものではなく、それがseq_cstの外部での順序を正式に保証する唯一のものです。

実行時にこの並べ替えが実際に発生する可能性がある、一貫性のある共有キャッシュを備えたあらゆる種類の「通常の」CPUは、妥当とは思われません。(しかし、コンパイラは削除することができればdummy1dummy2、その後明らかに我々は問題を抱えているだろう、と私は標準によって許可さだと思います。)

ただし、C ++のメモリモデルは、ストアバッファー、共有コヒーレントキャッシュ、または許可された並べ替えのリトマステストの観点から定義されていないため、C ++ルールでは、健全性に必要なものは正式には必要ありません。これは、スレッドプライベートであることが判明したseq_cst変数でさえも最適化できるようにするためのものです。(もちろん、現在のコンパイラーはそれを行いません。もちろん、アトミックオブジェクトの他の最適化も行いません。)

あるスレッドが実際にset()最後に見えても、別のスレッドがset()最初の音を信じられないほどに見える実装。POWERでさえそれはできませんでした。seq_cstのロードとストアの両方に、POWERの完全なバリアが含まれています。(私はコメントでIRIWの並べ替えがここに関係するかもしれないと提案しました; C ++のacq / relルールはそれに対応するのに十分弱いですが、同期または他の発生前の状況以外の保証の完全な欠如は、どのHWよりもはるかに弱いです。 )

C ++ 、実際にオブザーバーが存在しない限り、非seq_cstについては何も保証せず、そのオブザーバーに対してのみ保証します。 1人がいないと、シュレーディンガーの猫の領域にいることになります。または、2本の木が森に落ちた場合、一方が他方より先に落ちましたか?(それが大きな森である場合、一般相対性理論はそれが観測者に依存し、同時性の普遍的な概念は存在しないと言います。)


@mpoeterは、seq_cstオブジェクトに対しても、コンパイラーがダミーのロードおよびストア操作を削除することさえできると提案しました。

何も操作と同期できないことが証明できれば、それは正しいと思う。たとえば、dummy2関数をエスケープしないことを確認できるコンパイラは、おそらくそのseq_cstロードを削除できます。

これにより、少なくとも1つの現実的な結果が生じます。AArch64向けにコンパイルすると、以前のseq_cstストアが実際に後で緩和された操作で並べ替えることができます。後でロードを実行できます。

もちろん、現在のコンパイラは、ISO C ++がアトミックを禁止していなくても、アトミックをまったく最適化していません。これは標準委員会にとって未解決の問題です。

C ++メモリモデルには暗黙のオブザーバーやすべてのスレッドが順序付けに同意するという要件がないため、これは可能だと思います。コヒーレントキャッシュに基づいていくつかの保証を提供しますが、すべてのスレッドが同時に見えるようにする必要はありません。


いいまとめ!実際には、スレッドAだけがseq-cstフェンスを持っていれば、おそらくそれで十分であることに同意します。ただし、C ++標準に基づいて、からの最新の値が表示されるという必要な保証がないset()ため、スレッドBでもフェンスを使用します。とにかく、seq-cstフェンスを持つrelaxed-storeはseq-cst-storeとほぼ同じコードを生成すると思います。
mpoeter

@mpoeter:うん、私は実際には話をしていましたが、正式ではありませんでした。そのセクションの最後にメモを追加しました。そして、はい、実際には、ほとんどのISAでseq_cstストアは通常、単なるプレーン(リラックス)+バリアであると思います。か否か; POWERでは、seq-cstストアはストアのsync 前に(重い)行い、その後は行いません。godbolt.org/z/mAr72P ただし、seq-cstのロードには両側にいくつかのバリアが必要です。
Peter Cordes

1

ISO標準std :: mutexでは、seq_cstではなく、取得と解放の順序のみが保証されています。

ただし、「seq_cst順序付け」が保証されているものはseq_cstありません。これは、操作のプロパティではないためです。

seq_cstは、特定の実装std::atomicまたは代替のアトミッククラスのすべての操作に対する保証です。そのため、あなたの質問は不健全です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.