CPUキャッシュ完全ガイド:L1/L2/L3の仕組み・コヒーレンシーと実践的最適化テクニック

CPUキャッシュとは — 概要

CPUキャッシュは、プロセッサと主記憶(DRAM)の間に位置する高速なメモリ層で、メモリアクセスのレイテンシと帯域幅のギャップを埋めるための仕組みです。プログラムがアクセスするデータや命令のうち、利用頻度が高いものをキャッシュに保持することで、平均的なメモリアクセス時間(平均アクセス時間; AMAT)を大幅に低減します。

キャッシュの階層構造(L1/L2/L3)

現代のCPUは複数レベルのキャッシュを持ちます。一般的な構成は以下の通りです。

  • L1(最上位): コア単位。非常に高速だが容量は小さい(典型的には命令用32KB、データ用32KB程度)。アクセス遅延が最小。
  • L2: コア単位または小グループ単位。L1より大きく、遅延もやや大きい(数百KB〜1MB程度)。
  • L3(Last-Level Cache, LLC): コア共有。より大容量(数MB〜数十MB)だがレイテンシは高い。多コア環境での共有キャッシュとして機能。

ただし、サイズ・階層構成はマイクロアーキテクチャによって異なります(例: L1は通常非常に高速かつ小容量、L3は設計によってinclusive / exclusive / non-inclusiveといった方針が異なる)。

キャッシュの基本要素

主な用語と概念を整理します。

  • キャッシュライン(ブロック): データの転送単位。x86/ARMでは一般に64バイトがデファクト標準。
  • セット/ウェイ(連想度): キャッシュは「セット数 × 連想度(ウェイ数)」で構成される。直接マップ(1-way)、完全連想、セット連想(例: 8-way)など。
  • インデックス/タグ: オフセット・インデックス・タグによりアドレスを分割し、どのセット/行に格納されているかを判断する。
  • 置換ポリシー: LRU(最も使われていないものを置換)やその近似、ランダムなど。
  • 書き込みポリシー: write-back(通常は一般的)とwrite-through。書き戻しでは変更はキャッシュに留め、必要時に主記憶へ書き出す。

マッピング方式(直接・連想・セット連想)

キャッシュがどのように主記憶のアドレスを格納するかは性能に直結します。

  • 直接マップ: 各アドレスは1つの決まった行にマップされ、実装が簡単だが競合が発生しやすい。
  • 完全連想: 任意の行に置けるため競合は少ないが検索コスト・実装コストが高い。
  • セット連想(一般的): インデックスでセットを決め、そのセット内で複数ウェイに格納。競合と実装コストのバランスが良い。

データ一貫性とコヒーレンシー(MESI 等)

マルチコア環境では、各コアのキャッシュに同一アドレスのコピーが存在することがあるため、一貫性(coherency)を保つプロトコルが必要です。代表的なものがMESI(Modified, Exclusive, Shared, Invalid)です。拡張としてMOESI(Ownerを追加)などもあります。これらは「あるコアがデータを書き換えた場合に他のコアのコピーをどのように無効化/更新するか」を定義します。

仮想アドレスと物理アドレス — VIPT/PIPT

L1キャッシュはアクセス速度を上げるためにTLB(仮想→物理変換)と並行して動作させる設計があります。代表的な設計は以下:

  • PIPT(Physical Index, Physical Tag): タグとインデックスともに物理アドレス。TLB変換が完了するまでアクセスできない。
  • VIPT(Virtual Index, Physical Tag): インデックスに仮想アドレスを使い、TLB変換と並列でアクセス可能。ただしページ同義(aliasing)問題やページサイズの制約がある。

設計はトレードオフで、マイクロアーキテクチャにより選択が異なります。

プリフェッチとヒント

ハードウェア(およびソフトウェア)プリフェッチは、将来使われそうなデータを先読みしてキャッシュに入れておく機能です。シーケンシャルアクセスや一定ストライドのアクセスを検出して動作することが多く、適切に働くとメモリ待ち時間を隠蔽できます。ただし誤ったパターン検出や過剰プリフェッチは帯域を浪費して逆効果になることがあります。

キャッシュ関連のパフォーマンス指標と計測

パフォーマンスを評価する指標は複数あります。

  • キャッシュヒット率 / ミス率: 単純だが重要な指標。
  • ミスによる平均遅延(Miss penalty): L1ミスがL2ミスならばさらに遅延が増える。
  • AMAT(Average Memory Access Time): ヒット率とミス遅延を組み合わせた期待値。

測定ツールの例:

  • Linux perf(perf stat -e cache-misses,cpu-cycles など)
  • Valgrind cachegrind(シミュレータ)
  • Intel VTune、Intel PCM、likwid

プログラミング視点での最適化手法

キャッシュを意識したコード設計は大きな性能向上をもたらします。主要なポイントは「局所性(局所性の原理)」です。

  • 空間的局所性: 配列や構造体を連続領域に置く。連続アクセス(ストリーム)ではプリフェッチが効く。
  • 時間的局所性: よく使うデータをループ内に保持する。再利用を増やす。
  • データレイアウト: Structure of Arrays(SoA)とArray of Structures(AoS)の使い分け。
  • ループ変換(ループ交換・ループタイル/ブロッキング): 大きな行列乗算等ではブロッキングによりキャッシュ再利用を高める。
  • データ整列(alignment): キャッシュライン境界に合わせることで分割アクセスを避ける。
  • 偽共有(false sharing)の回避: 異なるスレッドが同一キャッシュライン内の別の変数を書き込むと無駄なコヒーレンシートラフィックが発生する。パディングで回避。
  • メモリ確保の工夫(ページカラーリングなど): 仮想→物理配置がキャッシュ競合に影響する場合がある。

書き込みポリシー詳細(write-back vs write-through)

write-back(書き戻し)は、データの変更をキャッシュ上で保持し、後でまとめて主記憶へ書き戻す方式です。通常は性能上有利で、現代のCPUで広く採用されています。write-throughは更新を即座に主記憶へ反映しますが、書き込みトラフィックが増えます。書き込みバッファや複数レベルの仕様により実装が複雑化します。

キャッシュコヒーレンシーとNUMA

巨大なマルチソケットシステムでは、各ソケットにキャッシュがあり、ソケット間の一貫性はディレクトリベースのプロトコルやインターコネクトで管理されます。NUMA環境ではメモリの「近さ」を意識した割り当て(メモリバインド)が重要です。

よくある誤解と注意点

  • 「L1に入れば無条件で高速」: L1ヒットは速いが、同時にTLBミスや分岐ミスが影響する場合がある。
  • 「キャッシュは万能」: プリフェッチの効果が限定的なアクセスパターン(ランダムアクセス)ではキャッシュ効果は小さい。
  • 「大きければ常に良い」: 大きなキャッシュはヒット率を上げるがレイテンシや消費電力、設計コストのトレードオフがある。

実践的なチェックリスト(パフォーマンスチューニング時)

  • hot path(頻繁に実行される部分)を特定する(プロファイラを利用)。
  • データアクセスパターンを解析し、空間/時間的局所性を高める。
  • 大きなデータ構造はブロッキングで処理。
  • スレッド間の偽共有を確認し、必要に応じて構造体をパディング。
  • 可能ならコンパイラ最適化やSIMD命令の活用でメモリアクセスを削減。
  • perf/VTune/cachegrindでキャッシュミスの箇所を検証。

デバッグ/低レベル操作

キャッシュを直接操作する命令もあり、x86では CLFLUSH / CLFLUSHOPT / CLWB 等でキャッシュラインをフラッシュできます。これらは主にキャッシュ制御や持続性メモリ(PMEM)など特殊用途で使われます。また、CPUベンダーが提供するパフォーマンスカウンタを用いることで、実機での詳細な統計(ミス数、ヒット率)を取得できます。

まとめ

CPUキャッシュは現代コンピュータの性能を支える重要な要素です。キャッシュの構造(ラインサイズ、階層、連想度)、一貫性プロトコル、書き込み・プリフェッチの挙動を理解することは、効率的なソフトウェア設計とパフォーマンス最適化に直結します。特にマルチコア/NUMA環境では、キャッシュ周りの設計が性能を左右するため、プロファイリングとそれに基づく改善が不可欠です。

参考文献