メモリ一貫性モデル ─ 逐次一貫性からTSO/弱順序まで
マルチコアで「片方のコアの書き込みがなぜ別コアから順番通りに見えないのか」を、ハードの内部動作から解けます。SC・TSO・弱順序の違いとフェンスの要否を原理で押さえ、ロックフリーの落とし穴を避けられます。
- 1.メモリ一貫性モデルは『あるコアの複数のメモリ操作が、他コアからどんな順序で観測されうるか』を定めるハードとプログラマの契約。最強の逐次一貫性(SC)は各コアのプログラム順を保ったインターリーブだけを許す。
- 2.x86のTSOはストアバッファ由来でStore→Loadの並べ替えのみ許し、Arm/RISC-Vの弱順序は依存のない4方向の並べ替えをほぼ全て許す。緩いほど高速だが、順序が必要な箇所にフェンスやacquire/releaseを自分で挿す必要がある。
- 3.データ競合のないプログラム(DRF)は、適切に同期すれば弱いハードでもSCと同じ結果になる(DRF-SC保証)。これが言語メモリモデルがロック・アトミックを介して提供する約束で、フェンスは過不足なく置くのが鉄則。
一貫性モデルとは何の契約か
単一コアなら、プログラムに書いた順にメモリが読み書きされたかのように見えます(命令は内部で順不同に走っても、結果はそう見える)。ところがマルチコアでは、あるコアが行った複数の書き込みが、別のコアからは書いた順とは違う順序で観測されることがあります。何が許され何が許されないかを定める規約が**メモリ一貫性モデル(メモリコンシステンシモデル)**です。
紛らわしいのはコヒーレンシとの違いです。コヒーレンシは「単一のアドレスについて、全コアが矛盾しない単一の値の履歴を見る」ことを保証する(MESIなどのプロトコルが担う)のに対し、一貫性モデルは「異なるアドレスへの複数操作の相対順序」を扱います。コヒーレントでも、別々のアドレスへの書き込みの順序が崩れることはあり得ます。
コヒーレンシ … 1つのアドレス内での値の整合(単一書き込み順)
一貫性モデル … 複数アドレスへの操作の、コア間で観測される順序
逐次一貫性(SC):最も直観的で最も遅い
Lamport が定義した**逐次一貫性(Sequential Consistency, SC)**は、「全コアの全メモリ操作を、各コア内のプログラム順を保ったまま1本の全順序に並べ直せる」ことを要求します。マルチコアの実行が、あたかも各コアの命令列をカードのようにシャッフル(インターリーブ)した1つの逐次実行と区別できない、という契約です。
古典的な例(Dekker風)で考えます。共有変数 X, Y は初期値0で、2コアが同時に走ります。
コアA: X = 1; r1 = Y;
コアB: Y = 1; r2 = X;
SCの下では、どうインターリーブしても r1 == 0 かつ r2 == 0 にはなりません。少なくとも片方のストアは相手のロードより前に全順序へ入るからです。ところが実機の多くは、この r1 == r2 == 0 を許してしまいます。SCは直観に合いますが、各ロード/ストアの完了を待ってから次へ進む必要があり、後述のストアバッファを活かせず遅いため、主流CPUはSCを採用しません。
なぜ崩れるのか:ストアバッファとStore→Loadの並べ替え
緩和の最大の源はストアバッファです。コアがストアを実行すると、値を即座にキャッシュへ反映するのではなく、まず自コア専用のストアバッファに積み、コアはロードなど後続命令へ先に進みます。キャッシュへの反映(=他コアへの可視化)はバッファから遅れて行われます。
ここで効くのがストアフォワーディングで、自分のロードは自分のストアバッファをまず覗き、同一アドレスがあればその値を即返します。つまり自分から見た自分のストアは即座に見えるのに、他コアからはまだ見えないという非対称が生まれます。
先の例で、コアAの X = 1 がストアバッファに留まったまま r1 = Y を先行実行し、コアBも対称に動くと、両ロードが相手のストアを観測する前に走り、r1 == r2 == 0 が成立します。これがStore→Loadの並べ替えです。プログラム順ではストアが先なのに、観測順ではロードが先に「すり抜ける」のです。
2命令の順序関係は「先がStore/Load × 後がStore/Load」の4通りあります。Store→Load/Store→Store/Load→Load/Load→Store のどれを並べ替え可能にするかで、モデルの強弱が決まります。許す方向が多いほど弱い(緩い)モデルです。
TSO(x86)と弱順序(Arm/RISC-V)
主要なハードのモデルを並べると、許す並べ替えの差が一目で分かります。
| モデル | 代表アーキ | Store→Load | Store→Store | Load→Load | Load→Store |
|---|---|---|---|---|---|
| SC | 理論モデル | 禁止 | 禁止 | 禁止 | 禁止 |
| TSO | x86 / SPARC | 許可 | 禁止 | 禁止 | 禁止 |
| 弱順序 | Arm / RISC-V / POWER | 許可 | 許可 | 許可 | 許可 |
TSO(Total Store Order)は x86 が採るモデルで、ストアバッファに起因するStore→Loadの並べ替えだけを許し、他の3方向は禁止します。ストア同士の順序は全コアで共通の単一順序(Total Store Order)に見え、ロード同士もプログラム順を保ちます。比較的強く、素朴に書いたコードでも壊れにくい一方、Store→Loadだけは例の r1 == r2 == 0 が起こるため、Dekkerロック等では明示的なフェンスが必要です。
弱順序(Weakly Ordered / Relaxed)は Arm・RISC-V・POWER が採り、データ依存のない限り4方向すべての並べ替えを許します。さらに弱いモデルでは、ストアの他コアへの伝播が非アトミックになり得ます(あるストアがコアCには見えてもコアDにはまだ見えない、いわゆるnon-multi-copy-atomic)。POWERや旧Armがこれに当たります。一方でRISC-VのRVWMOや、2017年改訂以降のArmv8はマルチコピーアトミックで(x86 TSOと同様)、ここまでは緩みません。自由度が高くハードを高速・低電力にできますが、順序が要る箇所をプログラマがすべて明示しなければなりません。だからこそ移植時に「x86では動いたコードがArmで壊れる」事故が起きます。
TSOはStore→StoreとLoad→Loadを保つため、フラグで完了を伝える定番パターン(データを書いてからreadyフラグを立てる→相手はreadyを見てからデータを読む)が、フェンス無しでも偶然動いてしまいます。これを弱順序のArmへ移植すると、ストアの並べ替えやロードの先読みでフラグとデータの順序が崩れ、まれに古いデータを読む競合が表面化します。移植性のためにモデルの差を仕様として正面から扱う必要があります。
フェンスとacquire/release意味論
並べ替えを止める道具が**メモリフェンス(メモリバリア)**です。フェンスはその前後のメモリ操作の追い越しを禁じます。x86のMFENCEは唯一残るStore→Loadを塞ぐ全順序バリア、ArmのDMB/RISC-VのFENCEは対象方向を指定できます。詳しい命令対応はメモリバリアの命令アーキテクチャで扱います。
実務で使うのは多くの場合、より目的指向なacquire/release意味論です。これは同期点に向きを与える考え方です。
- acquire(取得):以降のメモリ操作が、このロードより前へ抜けることを禁ずる(後続をこの点より後ろに留める「下向きの壁」)。ロック取得・フラグ読み取りに使う。
- release(解放):以前のメモリ操作が、このストアより後ろへ抜けることを禁ずる(先行をこの点より前に留める「上向きの壁」)。ロック解放・公開フラグの書き込みに使う。
release ストアが値Vを書き、別コアの acquire ロードがそのVを読んだとき、release より前の全書き込みが acquire 以降から見えることが保証されます。これがhappens-before(先行発生)関係を張る仕組みで、フルフェンスより制約が緩く、必要な片側だけを止めるので高速です。生産者・消費者やロックの正しさはこの一対で表現できます。
生産者(release): data = 42; // ① 普通の書き込み
ready.store(1, release); // ② これより前の①は前に留まる
消費者(acquire): if (ready.load(acquire) == 1) // ③ これより後は後ろに留まる
use(data); // ④ ①の結果(42)が必ず見える
データ競合とDRF保証
弱いハードでも安全に書けるのは、**データ競合(data race)**を定義し排除するからです。データ競合とは「同一アドレスへ、少なくとも一方が書き込みで、同期で順序付けられていない2つのアクセスが並行に起こる」状態です。
ここで核心がDRF-SC保証(Data-Race-Free implies Sequential Consistency)です。プログラムがデータ競合を持たない(=共有変数へのアクセスをロックや適切なメモリ順のアトミックで必ず同期する)なら、たとえ実機が弱順序でも、その実行はSCと区別できないことが保証されます。つまり競合さえ無くせば、プログラマは緩い実機の細部を意識せずSCの直観で推論できます。これがC++11やJavaなどの言語メモリモデルがロックやアトミックを介して与える約束であり、acquire/releaseやフェンスはこのDRFを満たすための道具です。
フェンスが足りなければ並べ替えで競合が表面化し、再現困難なバグになります。逆に過剰なフェンス(必要ない箇所のフルバリアや、迷ったときの seq_cst 乱用)はストアバッファとパイプラインを止め、ロックフリーの利点を帯域ごと消し去ります。弱順序ほど「どこで何の順序が要るか」をhappens-beforeで言語化し、その分だけのacquire/releaseを置くことが、正しさと性能の両立点です。
「SCはプログラム順を保つ全順序、TSOはStore→Loadのみ許可、弱順序は4方向許可」「Store→Loadの並べ替えの原因はストアバッファ+ストアフォワーディング」「acquireは後続を下に留め、releaseは先行を上に留める」「DRFならSC保証」は頻出です。コヒーレンシ(単一アドレス)と一貫性(複数アドレスの順序)の混同に注意してください。
まとめ
- 一貫性モデルは「複数アドレスへの操作がコア間でどんな順序で観測されうるか」の契約で、コヒーレンシ(単一アドレスの整合)とは別物。
- 緩和の源はストアバッファで、自コアのストアは即見えるが他コアには遅れて見える非対称が
Store→Loadの並べ替えを生む。 - x86のTSOは
Store→Loadだけ許す比較的強いモデル、Arm/RISC-Vの弱順序は4方向を許す。緩いほど速いが順序の明示が要る。 - acquire/releaseでhappens-beforeを張り、データ競合を排除すればDRF-SC保証で弱い実機でもSCの直観が使える。フェンスは過不足なく。
実装の土台となるストアバッファや投機の機構はアウトオブオーダ実行が、コア間でブロックを共有する経路はキャッシュメモリの原理とCXLコヒーレント接続が、フェンス命令の具体はメモリバリアの命令アーキテクチャが掘り下げます。
CPU/メモリ/ディスク Article
メモリ一貫性モデル ─ 逐次一貫性からTSO/弱順序までを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
メモリ一貫性モデル
比較で見る軸
難易度: advanced / カテゴリ: CPU/メモリ/ディスク / タグ数: 6
導入後に効く点
x86のTSOはストアバッファ由来でStore→Loadの並べ替えのみ許し、Arm/RISC-Vの弱順序は依存のない4方向の並べ替えをほぼ全て許す。緩いほど高速だが、順序が必要な箇所にフェンスやacquire/releaseを自分で挿す必要がある。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- CPU/メモリ/ディスク
- タグ数
- 6
判断チェックリスト
- 自社の用途が「メモリ一貫性モデル / 逐次一貫性」に近いか確認する。
- 強みである「メモリ一貫性モデルは『あるコアの複数のメモリ操作が、他コアからどんな順序で観測されうるか』を定めるハードとプログラマの契約。最強の逐次一貫性(SC)は各コアのプログラム順を保ったインターリーブだけを許す。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。