メモリ階層とキャッシュコヒーレンシ(MESIプロトコル)
マルチコアで各コアが同じデータを別々にキャッシュしても値が壊れないのはなぜか。MESIプロトコルとフォルスシェアリングの正体を、ハードウェアの動作から掴めます。
- 1.CPUはL1/L2/L3とメインメモリの階層を持ち、上位ほど速く小さい。データは固定長のキャッシュライン(典型64バイト)単位で出し入れされる。
- 2.各コアが同じラインを別々にキャッシュしても値が矛盾しないよう、MESIプロトコルが各ラインをModified/Exclusive/Shared/Invalidの4状態で管理し、書き込み時に他コアのコピーを無効化する。
- 3.無関係な変数が同じ64バイトラインに同居すると、片方への書き込みが他方のキャッシュを無効化するフォルスシェアリングが起き、並列性能が落ちる。パディングで分離する。
メモリ階層:速度と容量のトレードオフ
CPU から見たメモリは1枚ではなく、速いが小さい上位 から 遅いが大きい下位 へと連なる階層です。レジスタの次に来るのがキャッシュで、典型的には3段あります。
| 階層 | おおよその容量 | アクセス遅延の目安 | 共有範囲 |
|---|---|---|---|
| L1(命令/データ別) | コアあたり 32〜64KB | 約 4 サイクル | コア専用 |
| L2 | コアあたり 256KB〜1MB | 約 12 サイクル | コア専用(共有設計もあり) |
| L3(LLC) | 数MB〜数十MB | 約 40 サイクル | 全コア共有 |
| メインメモリ(DRAM) | 数GB〜数百GB | 約 200 サイクル超 | 全体共有 |
上位が小さいのは、SRAM が高速だが高価でダイ面積を食うからです。下位の DRAM は安く大容量だが遅い。この差を埋めるため、CPU はアクセスした その近傍 を先に上げておき、参照の局所性(直近に触ったデータと、その隣を再び触りやすい性質)を利用してヒット率を稼ぎます。L3 は全コアで共有され、メインメモリへの最後の砦(Last Level Cache)として働きます。
キャッシュライン:出し入れの最小単位
キャッシュはバイト単位ではなく、キャッシュライン という固定長ブロック単位でメモリと往復します。x86-64 や多くの ARM で 64バイト が標準です。1バイト読むだけでも、それを含む64バイト境界のラインが丸ごと上がってきます。
アドレス 0x1000 の1バイトを読む
→ 0x1000〜0x103F(64バイト)のラインがL1へ充填される
→ 隣接データもついでにキャッシュ済み(空間的局所性の活用)
この「64バイト一括」という性質が、後述するフォルスシェアリングの根本原因になります。まずは正しさ、つまり 複数コアが同じラインを持っても値が矛盾しない仕組み を見ます。
小さすぎると、まとめて運ぶ利点(バースト転送の効率)が薄れ、タグ管理のオーバーヘッドが相対的に増えます。大きすぎると、実際には使わない部分まで運ぶ無駄と、フォルスシェアリングの危険が増します。64バイトは多くのワークロードでの妥協点として広く採用されています。
なぜコヒーレンシが要るのか
各コアは自分の L1/L2 に独立したコピーを持てます。コア0が変数 x を読んで L1 に載せ、コア1も同じ x を載せる。ここでコア0が x を書き換えると、コア1の L1 には古い値が残ったまま になります。コア1がそれを読めば、書かれたはずの値が見えません。
この「全コアが、ある番地について 同じ最新の値 を観測できる」性質が キャッシュコヒーレンシ(一貫性) です。これをハードウェアが自動で保証する代表的な仕組みが MESIプロトコル です。
コヒーレンシは「1つの番地について全コアが最終的に同じ値を見る」ことの保証です。一方、複数の番地への操作が他コアにどんな順序で見えるか は別の問題で、そちらは メモリオーダリングとメモリバリア の領域です。MESI が値の一致を保証しても、ストアバッファ由来の並べ替えは残るので、両者を混同しないでください。
MESIプロトコル:4状態でラインを管理する
MESI は各コアのキャッシュ内の 各ライン に、次の4状態のいずれかを持たせます。頭文字を取って MESI です。
| 状態 | 意味 | 他コアの同ライン | メモリとの一致 |
|---|---|---|---|
| Modified(M) | 自分だけが持ち、書き換え済み(ダーティ) | 持てない | 不一致(自分が最新) |
| Exclusive(E) | 自分だけが持つが、未変更(クリーン) | 持てない | 一致 |
| Shared(S) | 複数コアが読み取り共有中 | 持ちうる | 一致 |
| Invalid(I) | 無効。中身は信用できない | 無関係 | — |
肝は M と E は「自分だけが持つ」排他状態、S は「複数で共有」、I は「持っていないのと同じ」 という区別です。コアがラインを 書き込める のは、それを排他的に持つ M か E のときだけ。S や I の状態で書こうとすると、まず排他権を取りに行きます。
書き込みの流れを追うと理解できます。
コア1が、S状態で持つラインに書き込みたい
1. バスへ "Invalidate(無効化)要求" を投げる
2. 同ラインをSで持つ他コアは、自分のコピーを I へ落とす
3. コア1のラインは S → M へ遷移し、書き込みを実行
→ 以降そのラインは「コア1だけが持つ最新版」になる
逆に、I のラインを 読みたい ときは「読み取り要求」を投げます。誰も持っていなければメモリ(または L3)から取って E、既に他コアが持っていれば共有して S になります。E から自分が書けば S を経ずに M へ 進めるため、競合のない単独アクセスは無効化通信なしで高速に書けます。これが E 状態を設けた理由です。
他コアの要求を監視して自分の状態を更新する方式を スヌープ(バススヌーピング) と呼びます。少数コアの共有バスでは有効ですが、コア数が増えると要求のブロードキャストが詰まります。多コア・マルチソケットでは、どのコアがどのラインを持つかを一覧で管理する ディレクトリ方式 に切り替え、無効化を必要なコアだけへ送ってトラフィックを抑えます。
MESIF/MOESI:転送と共有ダーティの最適化
MESI には弱点があります。あるラインを複数コアが S で共有しているとき、別コアがそれを読みたいと要求すると、誰が応答すべきか が曖昧で、結局メモリから読み直すことになりがちです。これを改良した派生が広く使われています。
- MESIF(Intel系): S を細分化し、共有コピーのうち1つだけを F(Forward) に指定する。読み取り要求には F を持つコアが代表して応答するため、メモリアクセスを避けてキャッシュ間で素早く転送できる。F は最後に読み込んだコアへ移っていく。
- MOESI(AMD系/一部ARM): O(Owned) を追加する。M のラインを他コアが読みたいとき、MESI では一度メモリへ書き戻して S にする必要があるが、MOESI では書き戻さずに O(自分が最新を保持しつつ他コアへ S で共有) へ落とせる。メモリとは不一致のまま共有を許す点が新しい。
分岐の系統(読み取り共有時の応答役を誰が担うか)
MESI ── 共有時は代表応答役なし → メモリから読み直しがち
├─ MESIF:F状態を1つ立て、Fが代表応答(Intel)
└─ MOESI:O状態で「ダーティのまま共有」を許す(AMD)
いずれも目的は同じで、キャッシュ間の直接転送を増やし、遅いメモリアクセスを減らす ことです。状態数は増えますが、基本の M/E/S/I の意味は保たれます。
フォルスシェアリング:偽りの共有による性能低下
ここでメモリ階層とコヒーレンシが結びつきます。MESI は キャッシュライン単位 で状態を管理するため、論理的には無関係な2つの変数でも、同じ64バイトラインに同居していると1つの単位として扱われます。
struct {
long a; // コア0が頻繁に更新
long b; // コア1が頻繁に更新
} s; // a と b が同じ64バイトラインに同居しがち
コア0が a を書くたびに、そのラインは M になり、b しか触っていないコア1のコピーまで I へ無効化 されます。コア1は次に b を読むだけでミスし、ラインを取り直す。互いに相手のラインを無効化し合い、ラインがコア間を行き来する キャッシュラインの ping-pong が起きます。データは共有していないのに共有しているかのように振る舞うので フォルスシェアリング(偽共有) と呼びます。
フォルスシェアリングは 結果を間違えません。MESI が正しく動いているからこそ無効化が走るだけで、値は常に正しい。だからテストは通り、スケールしないという形でだけ表面化 します。コアを増やすほど ping-pong が激化し、並列化したのに遅くなることすらあります。アトミックなカウンタ配列や、スレッドごとの統計値を密に並べた構造が典型的な踏み台です。
対策は 競合する変数を別のラインへ追い出す ことです。間にダミーを詰めてラインサイズ境界に揃えます。
struct {
long a;
char pad[56]; // 64 - 8 = 56バイト詰めて a を独立ラインに
long b;
} s; // a と b が別ラインに分かれ、相互無効化が消える
C++ なら alignas(64)、Java なら @Contended(JEP 142)といった言語機能でも同じ分離ができます。逆に、読み取り専用で共有されるデータはむしろ詰めて1ラインに収める ほうがキャッシュ効率がよく、対策はあくまで「書き込みが競合する変数」に限る点が要です。
「キャッシュの出し入れ単位はラインで典型64バイト」「MESI の M/E/S/I は、書き込めるのは排他状態(M/E)だけ・書き込み時に他コアを Invalidate する」「フォルスシェアリングは無関係変数の同一ライン同居が原因で、正しさは保たれ性能だけ落ちる」の3点は頻出です。E 状態がある理由(単独書き込みを無効化通信なしで速くする)も問われます。
まとめ
CPU は L1/L2/L3 とメインメモリの階層 を持ち、上位ほど速く小さい。データは キャッシュライン(典型64バイト) 単位で出し入れされます。マルチコアで各コアが同じラインを別々に持っても値が矛盾しないよう、MESIプロトコル が各ラインを Modified / Exclusive / Shared / Invalid で管理し、書き込めるのは排他状態のときだけ・書き込み時に他コアのコピーを Invalidate することで一貫性を保ちます。MESIF / MOESI はキャッシュ間転送を増やしてメモリアクセスを減らす最適化です。そしてライン単位管理の副作用が フォルスシェアリング で、無関係な変数の同居が相互無効化を招き、正しさは保ったまま並列性能だけを削ります。対策はパディングによる分離です。並べ替えとの関係は メモリオーダリングとメモリバリア、競合データを触るスレッド側の設計は スレッドプール や 排他制御とデッドロック も合わせてどうぞ。
OS Article
メモリ階層とキャッシュコヒーレンシ(MESIプロトコル)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
キャッシュコヒーレンシ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 5
導入後に効く点
各コアが同じラインを別々にキャッシュしても値が矛盾しないよう、MESIプロトコルが各ラインをModified/Exclusive/Shared/Invalidの4状態で管理し、書き込み時に他コアのコピーを無効化する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 5
判断チェックリスト
- 自社の用途が「キャッシュコヒーレンシ / MESI」に近いか確認する。
- 強みである「CPUはL1/L2/L3とメインメモリの階層を持ち、上位ほど速く小さい。データは固定長のキャッシュライン(典型64バイト)単位で出し入れされる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。