L1キャッシュ完全ガイド:仕組み・設計・コヒーレンシから実践的最適化・計測手法まで

はじめに

L1キャッシュ(レベル1キャッシュ)は、CPUに内蔵される最も高速で最小のキャッシュメモリで、プロセッサコアと下位のキャッシュ(L2、L3)あるいは主記憶(DRAM)との間の速度差を埋め、命令実行のレイテンシを低減する役割を担います。本稿ではL1キャッシュの基本概念から実装の特徴、動作原理、マルチコア環境での扱い、アプリケーション側での最適化手法や計測方法まで、深掘りして解説します。

L1キャッシュの基本

L1キャッシュは通常、各コアごとに専有(private)されており、命令用(I-cache)とデータ用(D-cache)に分割された「分離型(split)」構成が一般的です(ハーバードアーキテクチャ的分離)。主な特徴は以下の通りです。

  • サイズ:小さめ(典型的に16KB~64KB程度/コア)。近年のx86やARMコアでは32KB(I)+32KB(D)がよく見られます。
  • アクセス速度:非常に高速で、数サイクル(概ね1〜4サイクルのオーダー)で応答することが多い。
  • キャッシュラインサイズ:一般に64バイトが標準的。
  • 組合せ(アソシアティビティ):2〜8ウェイ程度が一般的で、衝突による競合を低減するために利用される。

キャッシュの動作原理(ヒット/ミス)

CPUがメモリアクセスを行うとき、まずL1キャッシュに該当データ(または命令)があるか確認します。見つかれば「ヒット」で高速応答、見つからなければ「ミス」となり、下位のキャッシュやDRAMからデータを取得してL1へ格納します。ミスには主に以下の種類があります。

  • コンパルショナルミス(compulsory / cold miss):初回アクセス時に起こるミス。
  • キャパシティミス(capacity miss):キャッシュ容量不足により置換が必要になった場合のミス。
  • コンフリクトミス(conflict miss):マッピングの性質で同一セットに複数ブロックが競合した場合のミス(直接マップや低ウェイ数のときに顕著)。

置換ポリシー・書き込みポリシー・プレフェッチ

キャッシュブロックをどれと入れ替えるかは置換ポリシーに依存します。ハードウェアでは完全なLRUは高コストのため、疑似LRU(pseudo-LRU)やLRU近似アルゴリズムが用いられます。書き込み時の挙動は主に以下の2種です。

  • Write-through:データをキャッシュに書き込むと同時に下位にも書き込む。実装は単純だが下位への書き込み頻度が増える。
  • Write-back:キャッシュ上でのみ更新し、ブロックが追い出されるときに下位へ書き込む。書き込み量を抑えられるが「ダーティ」ブロック管理が必要。

現代の多くのL1データキャッシュは書き戻し(write-back)方式を採用しています。さらに、ハードウェアによるプリフェッチ(次に使いそうなデータを先読みする機能)も性能向上に寄与しますが、誤ったプリフェッチは帯域浪費やキャッシュ污染を招くためバランスが重要です。

インデックスとタグ:仮想アドレスと物理アドレスの取り扱い

L1キャッシュ設計では「仮想→物理」アドレス処理が重要です。以下の方式が一般的です。

  • 物理インデックス / 物理タグ(PIPT):インデックスもタグも物理アドレスに基づくため、シノニム(別の仮想アドレスが同じ物理ページを参照する問題)に安全。ただし、物理変換(TLB参照)を待つ必要があるため遅延が増える。
  • 仮想インデックス / 物理タグ(VIPT):インデックスは仮想アドレス、タグは物理アドレス。TLBと並列で索引でき、レイテンシを抑えられるが、ページサイズやキャッシュのサイズ・アソシアティビティに制約がある(エイリアシング問題)。

多くのモダンな設計はVIPTを工夫して採用し、低レイテンシかつ整合性を保つようにしています。

マルチコア環境での整合性(コヒーレンシ)と階層設計

複数コアが存在する場合、各コアが持つL1キャッシュ間で同一メモリ位置に対する整合性を保つ必要があります。これを担うのがキャッシュコヒーレンシプロトコルで、代表的なものにMESIやMOESIがあります。これらはキャッシュラインごとに状態(Modified, Exclusive, Shared, Invalid 等)を管理して、書き込みや読み取り時の動作を制御します。

階層設計としては、L1は各コアに専有、L2はコア専用あるいは共有、L3は多くの場合複数コアで共有されることが多いです。階層の設計(inclusive, exclusive, non-inclusive)は、データの複製有無やミス処理時の帯域とレイテンシに影響します。

ソフトウェア側から見た影響と最適化技法

L1キャッシュは小容量かつ高速なため、ソフトウェア設計で意識すると大きな性能差が生まれます。主要な最適化テクニックを挙げます。

  • 局所性の確保:時間的局所性(同じデータを頻繁に使う)と空間的局所性(近接アドレスをまとめて扱う)を意識したデータ配置。
  • データレイアウト:配列アクセスは連続メモリを使う(SoA:Structure of Arraysが多くの場合有利)。ポインタチェーンの多いランダムアクセスはL1ミスを増やす。
  • ブロッキング/タイル化:行列演算などではループをブロック化して作業セットをL1に収める。
  • アラインメント:キャッシュライン境界にデータを揃えることでラインスプリットを避ける。
  • プリフェッチの利用:__builtin_prefetchなどで先読みを指示する。ただし自動プリフェッチと競合する可能性もあるのでプロファイルに基づく調整が必要。
  • 排他性の回避:複数スレッドで同一のキャッシュラインを書き換すと「false sharing」により性能低下。独立したキャッシュラインに分割する。

計測とプロファイリング

L1キャッシュの効果を測るためにはハードウェアパフォーマンスカウンタを使います。Linuxではperf、IntelではVTune、AMDではCodeXLなどが利用可能です。主に見るべき指標は以下の通りです。

  • L1データ/命令キャッシュミス数(cache-misses, L1-dcache-load-misses など)
  • キャッシュ参照回数(cache-references)
  • メモリ帯域使用量やTLBミス

これらをプロファイルし、ホットスポットのアクセスパターンを変えることで効果的な最適化が可能です。

実際の設計例と注意点

実例として、近年の多くの汎用プロセッサ(x86やARMコア)では「32KBのL1命令キャッシュ+32KBのL1データキャッシュ/コア、ライン長64バイト、4〜8ウェイ」程度の構成が普及しています。ただしコア設計や用途(モバイル向け、サーバ向け)により最適解は変わります。例えば、組み込み向けコアではより小さなL1や異なるアソシアティビティを採ることもあります。

最適化の際の注意点:

  • 微妙なハードウェア依存:最適化はプロセッサ世代・実装に依存するため、ベンチマークとプロファイルなしに一般化しないこと。
  • プリフェッチや低レイテンシ機構は逆効果になる場合があるため、必ず計測すること。
  • 高水準言語やランタイム(ガベージコレクタなど)の振る舞いもL1の効率に影響を与える。

まとめ

L1キャッシュはCPU性能に直接影響する重要な要素で、サイズや遅延、ライン長、アソシアティビティ、置換・書き込みポリシーなどの設計上のトレードオフがあります。ソフトウェア側で局所性を高め、false sharingを避け、アルゴリズムをキャッシュフレンドリーにすることで大きな性能向上が期待できます。プロファイルに基づくチューニングとハードウェア特性の理解が鍵です。

参考文献