フォルスシェアリングとキャッシュライン競合の原理
並列化したのに遅い、コアを増やすほど落ちる──その犯人がフォルスシェアリングです。無関係なデータが同じ64Bラインに同居する仕組みと、パディングで消す原理を掴めます。
- 1.コヒーレンスはバイト単位ではなくキャッシュライン(典型64B)単位で管理されるため、論理的に無関係な変数でも同一ラインに乗ると互いの書き込みで無効化し合う。
- 2.片方のコアの書き込みがもう片方のラインコピーを Invalid にし、所有権がコア間を往復するピンポンが起き、3C(初期参照・容量・競合)に加わるコヒーレンスミスとして現れる。
- 3.ラインパディング・アラインメント・スレッドローカル化でホットな変数を別ラインへ分離すれば、無効化トラフィックが消えて並列スケールが回復する。
コヒーレンスの単位はラインであってバイトではない
マルチコアで「並列化したのに速くならない、むしろコアを増やすほど遅い」という現象の典型的な犯人がフォルスシェアリング(偽共有)です。原因は一点に集約されます──キャッシュコヒーレンシはキャッシュライン(多くの x86/ARM 実装で 64 バイト)単位で管理され、バイト単位ではないことです。
各コアは自分の L1/L2 に同じラインのコピーを持てますが、MESI などのコヒーレンシプロトコルは「あるコアがラインへ書くなら、他コアの同一ラインコピーをすべて無効化(Invalidate)してから書く」という規律で整合を保ちます。この無効化の粒度がラインなので、そのライン上のどのバイトを書いたかは区別されません。論理的に無関係な 2 つの変数でも、同じ 64B ラインに同居していれば、片方への書き込みがもう片方のコピーを巻き添えで無効化します。これがフォルスシェアリング、すなわち「共有していないのにハードウェアは共有しているとみなす」状態です。
ピンポンが起きる仕組み
具体例で追います。counter[0] をコア A が、counter[1] をコア B が更新する、互いに独立なはずのコードです。
struct {
long a; // コア A だけが更新
long b; // コア B だけが更新
} shared; // a と b は隣接 → 同一ラインに同居
a と b は隣接配置されるため、ほぼ確実に同じ 64B ラインに乗ります。すると次が繰り返されます。
- コア A が
aに書く。プロトコルは A のラインを **Modified(所有・他は無効)**にし、B 側のコピーを Invalid にする。 - コア B が
bに書こうとするが、B のコピーは Invalid。B は最新ラインを A から取り寄せ(あるいはディレクトリ経由で所有権を奪い)、自分が Modified になる。今度は A 側が Invalid。 - 次に A が
aに書くと、また B から奪い返す。
この所有権の往復をキャッシュラインピンポンと呼びます。実データ(a の値、b の値)は互いに不要なのに、ライン全体がコア間バスやコヒーレントインターコネクトを行き来します。各往復は L1 ヒット(約 1ns)ではなく、リモート L2/L3 やインターコネクト往復(数十 ns〜)になり、スループットが桁で落ちます。コア数を増やすほど競合者が増え、負のスケーリングさえ起こります。
フォルスシェアリングはデータ競合(race)ではありません。a と b へのアクセスは正しく直列化されており、結果は常に正しい。問題は純粋に性能です。だからアトミック変数やロックフリー構造を使っても、隣接配置のままなら無効化トラフィックは発生します。むしろアトミック命令(書き込み所有権を毎回要求する)はピンポンを増幅しがちで、ロックフリーカウンタを並べた配列が最悪のパターンになることがあります。
ミス分類への位置づけ:3C+コヒーレンス
単一コアのキャッシュミスは初期参照・容量・競合の**3C(Compulsory / Capacity / Conflict)に分類されますが、マルチコアではこれに第4のC=コヒーレンスミス(Coherence miss)**が加わります。フォルスシェアリングによるミスはこのコヒーレンスミスに属します。
| ミス種別 | 原因 | 容量を増やすと | 対策の方向 |
|---|---|---|---|
| Compulsory | 初回参照 | 消えない | プリフェッチ・大ブロック |
| Capacity | ワーキングセット超過 | 減る | 容量・局所性改善 |
| Conflict | 同一セットへ集中 | 減りにくい | 連想度・配置変更 |
| Coherence(真) | 他コアが本当に共有データを更新 | 消えない | 共有を減らす設計 |
| Coherence(偽=偽共有) | 無関係データが同一ライン | 消えない | ラインを分離 |
重要なのは、コヒーレンスミスは容量を増やしても消えない点です。L3 を倍にしても、無効化はライン単位の論理で起きるため無関係です。さらに偽共有は真の共有(True sharing)と区別が必要です。真の共有は複数コアが同じ論理データを実際に読み書きするため通信が本質的に必要ですが、偽共有は配置を変えれば通信そのものが消える──ここが対策可能性の分かれ目です。
偽共有は「特定の命令の遅さ」としては現れにくく、HITM(Hit Modified、他コアの Modified ラインへのアクセス)イベントの多発として観測されます。perf c2c(cache-to-cache)はまさにこの HITM を行・オフセット単位で集計するためのツールで、どの構造体のどのフィールドが同一ラインで競合しているかを特定できます。漫然と「遅い」ではなく、HITM とラインオフセットで裏取りするのが定石です。
防ぐ原理:ホットな変数を別ラインへ追い出す
対策はすべて「同時に別コアが書く変数を、物理的に別のキャッシュラインへ分離する」という一点に帰着します。
パディングとアラインメント
各コア専用のデータを 64B 境界に整列させ、ライン丸ごとを占有させます。
struct counter {
long value;
char pad[64 - sizeof(long)]; // 残りを埋め、次の counter を別ラインへ
} __attribute__((aligned(64)));
struct counter counters[NCORES]; // 各要素が独立したラインを占有
aligned(64) で先頭をライン境界に合わせ、pad でライン末尾まで埋めます。両方が要る点に注意してください。アラインメントだけだと隣接要素が同じラインの後半に食い込み、パディングだけだと配列先頭がライン中央から始まって 1 要素が 2 ラインにまたがり得ます。C++17 なら std::hardware_destructive_interference_size(=偽共有を避けるべき最小間隔)を使うと、64 という即値をハードコードせずに済みます。
配置・分離・スレッドローカル化
- 構造体のフィールド並べ替え: 読み取り専用(コア間で共有してよい)フィールドと、頻繁に書かれるフィールドを別ラインに分ける。ホットでミュータブルなフィールド同士も離す。
- スレッドローカル集約: 各スレッドが自分専用のローカル変数(別ライン、理想的には別ページ)に加算し、最後に一度だけ合算する。これが最も確実で、書き込み中のコヒーレンス通信をゼロにできる。ヒストグラムや統計カウンタの定番。
- 配列のストライド配置: スレッド i が
arr[i]ではなくarr[i * STRIDE]を使い、隣接スレッドのアクセスをライン境界より広く離す。 - 共有の最小化: そもそも書き込みを共有領域から追い出し、最後の縮約(reduction)だけで交わるよう設計を変える。
パディングは万能ではありません。1 つの long(8B)を 64B に膨らませると実効容量が 8 倍になり、キャッシュフットプリントが増えて Capacity ミスや TLB ミスを誘発します。偽共有が実測で問題になっているホットなデータにだけ適用し、冷たいデータや要素数の多い配列に機械的に撒かないこと。対策の前後で必ず perf c2c や実スループットを測り、トレードオフを確認します。
「コヒーレンスの粒度はバイトではなくライン(典型 64B)」「偽共有は正しさの問題ではなく性能の問題」「容量を増やしても消えない第4のC=コヒーレンスミス」「真の共有は通信必須だが偽共有は配置で消せる」「対策はパディング+アラインメント+スレッドローカル化」が頻出の核です。__attribute__((aligned(64))) とパディングが両方必要な理由を説明できると強い。
まとめ
- キャッシュコヒーレンシはキャッシュライン(典型 64B)単位で働くため、無関係な変数でも同一ラインに同居すると互いの書き込みで無効化し合う。
- 所有権がコア間を往復するラインピンポンで、L1 ヒット相当だったアクセスがインターコネクト往復に化け、スループットが桁落ち・負のスケーリングを招く。
- これは 3C に加わるコヒーレンスミスであり、容量増では消えず、偽共有は真の共有と違って配置変更で根絶できる。
- 対策はホットな変数を別ラインへ追い出すこと──64B アラインメント+パディング、スレッドローカル集約、ストライド配置が中心で、
perf c2cで裏取りする。
CPU 側でミスをどう隠すかはアウトオブオーダー実行が、依存とストールの基礎はパイプラインハザードが掘り下げます。
CPU/メモリ/ディスク Article
フォルスシェアリングとキャッシュライン競合の原理を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
フォルスシェアリング
比較で見る軸
難易度: advanced / カテゴリ: CPU/メモリ/ディスク / タグ数: 5
導入後に効く点
片方のコアの書き込みがもう片方のラインコピーを Invalid にし、所有権がコア間を往復するピンポンが起き、3C(初期参照・容量・競合)に加わるコヒーレンスミスとして現れる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- CPU/メモリ/ディスク
- タグ数
- 5
判断チェックリスト
- 自社の用途が「フォルスシェアリング / キャッシュコヒーレンシ」に近いか確認する。
- 強みである「コヒーレンスはバイト単位ではなくキャッシュライン(典型64B)単位で管理されるため、論理的に無関係な変数でも同一ラインに乗ると互いの書き込みで無効化し合う。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。