アトミック命令と同期プリミティブのハード実装
ロックやカウンタが壊れない理由を、命令レベルから把握できます。CAS・LL/SC・fetch-and-add がキャッシュコヒーレンスとどう連携し、バスロックからキャッシュロックへ進化したかを原理から解説します。
- 1.アトミックな read-modify-write は、対象キャッシュラインを排他(M/E)状態で確保し、その間コヒーレンスでスヌープを退ける『キャッシュロック』で実現する。バス全体を止める旧来のバスロックはアラインメント越えなど例外時のみに後退した。
- 2.x86 は CAS 系(lock cmpxchg)と fetch-and-add(lock xadd)を直接命令で持ち、ARM/RISC-V は LL/SC(load-linked / store-conditional)で楽観的に試みて失敗時に再試行する。両者はコヒーレンスの『書き込みで他コピーを無効化』という同じ土台に乗る。
- 3.競合が激しいとライン奪い合いでスループットが落ち、LL/SC はライブロックの危険を持つため、ハードはフォワードプログレス保証や near-memory のリモートアトミックで緩和する。
なぜ命令1つの「不可分性」が問題になるのか
counter = counter + 1 は高級言語では1行でも、機械語では「ロード→加算→ストア」の3段です。2つのコアが同時に実行すると、両方が同じ古い値をロードし、それぞれ加算してストアし、結果として加算が1回ぶん消える(ロストアップデート)。これを防ぐのがアトミック命令で、read-modify-write(RMW)の全体を、途中を誰にも観測・割り込みさせない不可分操作として実行します。ソフトのロックも突き詰めれば、こうしたハードのアトミック命令1個の上に組み上がっています。
不可分性は「実行中ずっと値を独占する」ことを意味します。鍵は、その独占をどの粒度で確保するかです。歴史的にはメモリバス全体を、現代では1本のキャッシュラインだけを占有します。
バスロックからキャッシュロックへ
初期の x86 では、lock 接頭辞付き命令はメモリバスにロック信号(LOCK#)を張り、RMW が終わるまで他のすべてのバスマスタのアクセスを止めていました。これがバスロックです。確実ですが、無関係なアドレスへのアクセスまで巻き添えで止まるため、コア数が増えるほど致命的なボトルネックになります。
現代の実装は**キャッシュロック(cache locking)**へ進化しました。RMW 対象のデータが1本のキャッシュラインに収まり、そのコアがラインをコヒーレンスプロトコルの M(Modified) または E(Exclusive) 状態で保持できるなら、バスを止める必要はありません。
- RMW 開始時、対象ラインを Read-For-Ownership(RFO) で要求し、他コアの全コピーを Invalid にする(排他取得)。
- ラインを M/E で握っている間に、ロード・演算・ストアを完了する。
- その間に来る他コアのスヌープ要求は、RMW が終わるまで応答を保留(または完了後に処理)し、外からは中間状態が一切見えないようにする。
つまりロックの単位がバス全体から「1ライン」へ縮み、無関係なメモリアクセスは並行して進めます。MESI などキャッシュコヒーレンスの「書き込み前に他コピーを無効化する」性質が、そのままアトミック性の土台になっている点が要です。バスロックは消滅したわけではなく、対象がラインをまたぐ(アラインメント違反)など、キャッシュロックで完結できない場合のフォールバックとして残ります。
アトミック変数がキャッシュラインの境界をまたいで配置されると、1本のラインを握るだけでは不可分性を保証できず、ハードはバスロック相当の重い経路に後退します。x86 では「split lock」と呼ばれ、数百〜数千サイクルの遅延と全コアへの波及を招くため、近年は検知して例外を上げる機構もあります。アトミック変数は必ず自然アラインメントに置くのが鉄則です。
主要プリミティブ:CAS・fetch-and-add・LL/SC
ハードが提供する RMW は大きく2系統あります。
| 方式 | 代表命令 | 意味 | 採用 | 性質 |
|---|---|---|---|---|
| compare-and-swap | lock cmpxchg (x86) | 期待値と一致したら新値を書く | x86, ARMv8.1 CAS | 条件付き単発、ABA問題あり |
| fetch-and-add | lock xadd (x86) | 加算し加算前の値を返す | x86, ARMv8.1 LDADD | 競合下でも必ず成功(待機自由) |
| LL/SC | ldxr/stxr (ARM), lr/sc (RISC-V) | 予約付きロード+条件付きストア | ARM, RISC-V, POWER | 汎用RMWを楽観的に合成、再試行前提 |
compare-and-swap(CAS) は「メモリの現在値が期待値と等しければ新値に差し替え、結果(成否や旧値)を返す」操作です。ロックフリーなデータ構造の中核ですが、値が A → B → A と戻ると変化を見逃す ABA 問題 を持ち、バージョンカウンタ併用などで対処します。x86 の lock cmpxchg が代表で、CAS の詳細なループ構成はロックフリーと CASが扱います。
fetch-and-add は「加算して加算前の値を返す」操作で、チケットロックや参照カウンタに向きます。CAS と違い競合下でも必ず一度で成功するため、後述のフォワードプログレスの観点で優れます。
LL/SC(load-linked / store-conditional) は RISC 系の手法です。load-linked で読むと同時にそのアドレスを**予約(reservation)**し、store-conditional は「予約が破られていなければ書いて成功、破られていれば書かずに失敗」を返します。予約はコヒーレンスと連動し、他コアがそのラインへ書き込む(無効化が届く)と予約が外れます。
// LL/SC で fetch-and-add を合成する典型形
retry:
ll r1, [addr] // 予約しつつロード
add r2, r1, #1
sc r3, r2, [addr] // 予約が生きていれば書いて r3=成功
beqz r3, retry // 失敗(他コアが割り込んだ)なら再試行
LL/SC は単一プリミティブで CAS・fetch-and-add・任意の RMW を表現できる柔軟さが強みですが、「失敗したら再試行」という楽観的な構造ゆえに、競合時はループが回り続けるリスクを抱えます。
競合時のフォワードプログレスとライブロック
複数コアが同じアトミック変数を激しく叩くと、キャッシュラインが M 状態のまま各コアの間を往復し続けます(キャッシュライン・バウンシング)。各 RFO は他コアの無効化とライン転送を伴うため、コア数に対してスループットが伸びず、むしろ悪化します。これは正しさの問題ではなく性能の問題ですが、LL/SC ではさらにライブロックという正しさに近い危険があります。
2コアが互いに同じラインへ LL/SC を試みると、コアAの ll 予約をコアBの ll(の RFO)が破り、Bの予約をAが破り…と続き、双方の sc が永遠に失敗し続ける可能性があります。誰も前進しない(ライブロック)状態です。これを避けるため ISA は通常、ll と sc の間に置ける命令を厳しく制限し、ハードは一定期間その予約にフォワードプログレス保証(少なくとも片方の sc が成功する)を与えます。さらにソフト側はランダム化したバックオフで衝突確率を下げます。
CAS ベースのループ(do { 旧値読む; 新値計算; } while(!CAS))もロックフリーではあります。これは「全体として少なくとも1スレッドは必ず前進する」保証で、システムが止まらないことを意味します。一方 fetch-and-add 系は個々のスレッドが有限ステップで完了する**待機自由(wait-free)**で、より強い公平性を持ちます。フォワードプログレスの強さは 待機自由 > ロックフリー > 障害なし(obstruction-free) の順で、ハードプリミティブの選択がこの保証レベルを直接左右します。
リモートアトミック:演算をメモリ近傍へ送る
ラインバウンシングの根本原因は「演算するためにデータをコアまで引き寄せる」ことです。そこで近年は発想を逆転させ、演算をデータの近くへ送るリモートアトミック(near-memory atomic)が広がりました。
ARMv8.1 の LDADD/SWP などの命令は、競合が激しいと判断されればコアでラインを取得せず、演算要求そのものを共有キャッシュ(LLC)やメモリコントローラ、相互接続のホームノードへ送り、そこで RMW を実行して結果だけ返せます。データはホーム側に居続けるため、コア間でラインを奪い合うバウンシングが起きません。x86 系でも xadd を LLC 側で処理する最適化が見られます。
この考え方は単一チップを超え、CXL のようなコヒーレント相互接続の世界へも延びます。デバイスやメモリプールに対するアトミックを、データの所在地(ホーム)で実行することで、長い往復遅延を避けつつ整合性を保ちます。near-memory アトミックは、高競合カウンタやヒストグラム集計のように「多数のコアが少数の変数を更新する」ワークロードで特に効きます。
lock 系命令は不可分性(アトミック性)に加え、x86 では完全なメモリバリアとしても働きますが、ARM の ldxr/stxr 単体は順序を保証しません。順序が要るなら acquire/release 付き(ldaxr/stlxr)を使います。「複数操作を1つに見せる(アトミック性)」と「操作の可視順序を制御する(オーダリング)」は独立した概念で、両方を区別して設計する必要があります。詳細はメモリオーダリングとバリアを参照してください。
「アトミック命令の正体は不可分な read-modify-write」「現代の実装はバスロックでなくキャッシュロック(対象ラインを M/E で排他確保)」「CAS は ABA 問題を持つ/LL/SC は再試行前提でライブロックの危険」「アラインメント越え(split lock)は重いバスロック経路に後退」は頻出です。アトミック性とメモリオーダリングが別概念である点も問われます。
まとめ
- アトミック命令は read-modify-write を不可分に実行し、ソフトのロックや lock-free 構造の最下層を支える。
- 実装はバスロックからキャッシュロックへ進化し、対象ラインを M/E 状態で排他確保してコヒーレンスのスヌープを退けることで、1ライン粒度の独占を実現する。
- CAS と fetch-and-add は専用命令、LL/SC は予約付きで汎用 RMW を楽観的に合成する。フォワードプログレスは待機自由>ロックフリーの順で強く、LL/SC はライブロック対策が要る。
- 高競合では near-memory のリモートアトミックが演算をデータ側へ送り、ラインバウンシングを回避する。
命令の並列実行の文脈はアウトオブオーダー実行が、ライン無効化の土台はキャッシュメモリの原理が掘り下げます。
CPU/メモリ/ディスク Article
アトミック命令と同期プリミティブのハード実装を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
アトミック命令
比較で見る軸
難易度: advanced / カテゴリ: CPU/メモリ/ディスク / タグ数: 5
導入後に効く点
x86 は CAS 系(lock cmpxchg)と fetch-and-add(lock xadd)を直接命令で持ち、ARM/RISC-V は LL/SC(load-linked / store-conditional)で楽観的に試みて失敗時に再試行する。両者はコヒーレンスの『書き込みで他コピーを無効化』という同じ土台に乗る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- CPU/メモリ/ディスク
- タグ数
- 5
判断チェックリスト
- 自社の用途が「アトミック命令 / CAS」に近いか確認する。
- 強みである「アトミックな read-modify-write は、対象キャッシュラインを排他(M/E)状態で確保し、その間コヒーレンスでスヌープを退ける『キャッシュロック』で実現する。バス全体を止める旧来のバスロックはアラインメント越えなど例外時のみに後退した。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。