yohhoyの日記

技術的メモをしていきたい日記

C++のフェンス is 何 in Practice

C++メモリモデルにおける フェンス(fence) とハードウェア・メモリバリア命令との対応関係についてメモ。

まとめ:

  • atomic_thread_fenceコンパイル時の並び替え禁止+メモリバリア命令発行(実行時の並び替え禁止)
  • atomic_signal_fenceコンパイル時の並び替え禁止のみ
  • C++標準ライブラリ提供のフェンス関数と、発行すべきメモリバリア命令セマンティクスとの対応は下表の通り。コンパイラによる並び替え禁止では、各メモリバリア命令と同じ制約をソースコードレベルで適用する。
memory_order #LoadStore #LoadLoad #StoreStore #StoreLoad
relaxed
acquire*1
release
acq_rel
seq_cst

例)acquireフェンスは#LoadStore+#LoadLoad並び替えを禁止、つまり該当フェンスに先行するload操作と同フェンスに続くload/store操作との並び替えを禁止する。*2

ノート:この対応表は、該当フェンスが要請するセマンティクスを満足するために、少なくとも禁止すべき並び替えを表している。対象プロセッサのハードウェア・メモリモデルによっては、追加操作なしに並び替え禁止が暗黙保証されるケース(例:x86 のような “強い” メモリモデル)や、命令セット・アーキテクチャ(ISA)設計によっては、細分化かつ直交したメモリバリア命令を提供しないケース(例:PowerPC では lwsync, hwsync(sync) 命令の2段階)もある。アーキテクチャ別の対応表は "C/C++11 mappings to processors" 参照。

現行C++11メモリモデルのフェンス・プリミティブはN2633で提案され、その後N2731で関数名が現行のものに変更された。これ以前のDraftでは、フェンス操作=特殊なグローバルオブジェクトに対するatomic操作*3と定義していた。

The motivation for having fence primitives in C++ is fourfold:

  1. It's hard to port existing code that is written for a specific fence-based architecture to the C++ acquire/release memory model. We want C++ to provide a way for rewriting such code in a manner that increases portability without sacrificing efficiency.
  2. Some programmers may be familiar with a fence-based memory model and, ideally, we want to help them be immediately productive in C++.
  3. In many real-world cases, the programmer knows exactly what sequence of instructions he wants to produce on the target hardware. We want to enable these programmers to achieve their goals while still writing portable code. (Stated simply, we want the programmer to be able to emit a lwsync instruction without sacrificing portability and theoretical correctness.)
  4. There are situations in which a fence-based formulation on the source level leads to more efficient code.
N2633 Improved support for bidirectional fences

Changes from N2633

  • Renamed atomic_memory_fence to atomic_thread_fence.
  • Renamed atomic_compiler_fence to atomic_signal_fence.
  • (snip)
N2731 Proposed Text for Bidirectional Fences

理論と実践のギャップ

C++メモリモデルとハードウェア・メモリモデルとでは、ルールの適用対象・構成要素・表現方法が異なる。

C++メモリモデル
C++ソースコードコンパイル時と実行時に対して、メモリアクセス間の「順序性」によってルールを定義する。この順序性は、atomic変数のacquire操作/release操作ペア または relaxed操作+フェンス関数のペア*4 により表現する。
ハードウェア・メモリモデル
機械語命令列の実行時に対して、メモリアクセス操作順入れ替えの “許容度合い” によってルールを定義する*5。メモリバリア命令の発行により、メモリアクセス入れ替えの禁止を表現する。*6
  • C++11標準ライブラリが提供するstd::atomic_thread_fence関数は、atomic変数アクセスに対してのみ意味をもつ。通常の非atomic変数には直接的な影響を与えない(間接的には影響を与えうる)。
  • C++の文脈における「フェンス(fence)」は、言語仕様を記述する仮想機械(abstract machine)上の同期プリミティブにすぎない。フェンス同士またはフェンス+atomic変数操作間で「happens before関係」が定義され、直接的にはそれ以上の効果を持たない。
  • 上記より、具体的なアーキテクチャコンパイラが提供するメモリバリア(memory barrier)とは解釈が異なる(ことが一般的なはず)。
C++のフェンス is 何

C++標準ライブラリのフェンス関数は、既存アーキテクチャのメモリバリア命令で制御されるハードウェア・メモリモデルを、acquire−release関係*7に基づき定義されるC++メモリモデルに統合*8する目的で導入された。特定ハードウェア上の双方向フェンス(メモリバリア命令)を用いて記述された既存コードから、C++標準ライブラリ/非seq_cstなatomic変数へのソースコード移植性を考慮し、特定のオブジェクトに紐付かない非メンバ関数として定義される。このためフェンス・プリミティブの動作セマンティクスは、メモリアクセス+双方向フェンスに基づくハードウェア・メモリモデルでの定義から、C++メモリモデル/仮想機械上のacquire−release関係へとマッピングしたものとなる(はず)。

関連URL

*1:フェンス関数に対するmemory_order_consume指定はmemory_order_acquireと等価。(C++11 29.8/p5)

*2:本記事ではメモリ一貫性モデル(memory consistency model)を扱うため、ここでの “操作の並び替え” とは、異なるatomic変数に対するload/store操作が対象となる。単一atomic変数上での操作の整合性(coherence)については、フェンスの議論とは関係なく、あらゆる並び替えが禁止されている。(C++11 1.10/p15-19)

*3:N2381当時の atomic_global_fence_compatibility オブジェクト+fence メンバ変数

*4:厳密には、atomic変数のacquire/release操作とrelease/acquireフェンス間でも順序付け関係が成立しうる。(C++11 29.8/p3-4)

*5:マルチプロセッサシステムでのキャッシュ・コヒーレンシ(cache coherence)制御や、プロセッサ内部のアウト・オブ・オーダー(OoO; Out-of-Order)実行により、メモリアクセス順序の入れ替えが発生する。ハードウェアによるメモリアクセス順序入れ替えは、先行命令よりも後続命令が “先行する”、または後続命令が先行命令を “追い越す” とも表現される。

*6:独立したメモリバリア命令のほかにも、それ自身でメモリバリアのセマンティクスをもつメモリアクセス命令も存在する。例:x86(IA32) の xchg 命令、Itanium(IA64) の ld.acq, st.rel 命令、ARMv8 の ldra, strl 命令など

*7:acquire操作−release操作の対により異なるスレッド間のsynchronized-with関係が定義され、そこからhappens-before関係が導出される。

*8:C++メモリモデルは、様々なハードウェア・メモリモデルを包含するモデルとも解釈できる。ただし、あらゆるハードウェア・メモリモデルを完全に表現できる訳ではない。