はじめに
CPUは、アプリケーションやOSを構成する命令を非常に高速に実行します。しかし、CPUが処理に必要なデータを保存しているメインメモリは、CPUと同じ速度で動作できるわけではありません。
もしCPUがデータを必要とするたびにメインメモリへアクセスすると、CPUはデータが届くまで待つ時間が増えてしまいます。CPU自体の処理能力が高くても、必要なデータをすぐに取得できなければ、その性能を十分に発揮できません。
そこで、CPUとメインメモリの速度差を埋めるために利用されるのがCPUキャッシュです。CPUキャッシュは、CPUが使用する可能性の高いデータを一時的に保持し、メインメモリよりも高速にデータを提供します。
本記事では、CPUキャッシュの役割、L1・L2・L3キャッシュの違い、Cache HitとCache Miss、Cache Line、マルチコアCPUにおけるキャッシュの関係について解説します。また、LinuxでCPUキャッシュの構成やCache Missを確認する方法についても説明します。
CPUキャッシュとは
CPUキャッシュとは、CPUとメインメモリの間に配置される高速な記憶領域です。CPUが直近で使用したデータや、これから使用する可能性が高いデータを一時的に保持します。
一般的なCPUでは、キャッシュはL1 Cache、L2 Cache、L3 Cacheという複数の階層に分かれています。単純化すると、次のような関係になります。

CPUに近いキャッシュほど高速ですが、容量は小さくなります。反対に、CPUから離れた階層ほど容量は大きくなりますが、アクセスにはより多くの時間が必要です。
CPUキャッシュの役割
CPUキャッシュの主な役割は、CPUがメインメモリへアクセスする回数を減らすことです。CPUが必要とするデータがキャッシュに存在すれば、メインメモリまでアクセスせずにデータを取得できます。
例えば、プログラムが同じ変数を繰り返し参照する場合、そのデータがキャッシュに保持されていれば、2回目以降のアクセスを高速に行える可能性があります。
なぜCPUキャッシュが必要なのか
CPUの動作速度は向上してきましたが、メインメモリへのアクセス速度はCPU内部の処理速度ほど速くありません。この速度差によって、CPUがメモリからデータを待つ時間が発生します。
CPUから見ると、メインメモリへのアクセスには多くのCPUサイクルが必要になることがあります。その間、後続の命令を進められなければ、CPU内部の実行部分が待ち状態になります。
CPUキャッシュは、よく利用されるデータをCPUの近くに置くことで、この待ち時間を減らします。つまりCPUキャッシュは、単なる一時保存領域ではなく、CPUの性能を引き出すために欠かせない仕組みです。
L1・L2・L3キャッシュの違い
CPUキャッシュは、速度と容量の異なる複数の階層から構成されます。代表的なものがL1 Cache、L2 Cache、L3 Cacheです。ここで使われるLはLevelを意味します。
L1 Cache
L1 CacheはCPUコアに最も近く、通常は最も高速なキャッシュです。その一方で、容量は他の階層より小さくなります。
L1 Cacheは、命令を保持するL1 Instruction Cacheと、データを保持するL1 Data Cacheに分かれているCPUが一般的です。これにより、CPUは実行する命令と処理対象のデータを効率よく取得できます。
L2 Cache
L2 CacheはL1 Cacheより容量が大きく、アクセス速度はL1 Cacheより遅いキャッシュです。多くのCPUでは、L2 CacheもCPUコアごとに用意されます。
CPUが必要とするデータをL1 Cacheで見つけられなかった場合、次にL2 Cacheを確認します。L2 Cacheにデータがあれば、L3 Cacheやメインメモリまでアクセスする必要はありません。
L3 Cache
L3 Cacheは、一般的にL1 CacheやL2 Cacheより大容量です。アクセス速度はL1・L2 Cacheより遅くなりますが、それでも通常はメインメモリより高速にアクセスできます。
マルチコアCPUでは、L3 Cacheが複数のCPUコアで共有される構成がよく利用されます。これにより、各コアのL1・L2 Cacheで見つからなかったデータを、共有されたL3 Cacheから取得できる可能性があります。
L1・L2・L3キャッシュの違いまとめ

| キャッシュ | 速度 | 容量 | 一般的な構成 |
|---|---|---|---|
| L1 Cache | 最も高速 | 最も小さい | コアごとに配置 |
| L2 Cache | L1より遅い | L1より大きい | コアごとに配置されることが多い |
| L3 Cache | L1・L2より遅い | 最も大きい | 複数コアで共有されることが多い |
※ただし、キャッシュの階層、容量、共有方法はCPUのアーキテクチャによって異なります。すべてのCPUが必ず同じ構成になっているわけではありません。
CPUはキャッシュをどのように利用するのか
CPUが命令を実行する際には、レジスタだけでなくメモリ上のデータも必要になります。CPUがあるアドレスのデータを読み込む場合、まずCPUに近いキャッシュからデータを探します。

必要なデータが上位のキャッシュにあれば、下位のキャッシュやメインメモリへアクセスする必要はありません。そのため、CPUが利用するデータをどれだけキャッシュから取得できるかが性能に大きく影響します。
Cache Hitとは
CPUが必要とするデータをキャッシュ内で見つけられることをCache Hitと呼びます。
例えば、必要なデータがL1 Cacheに存在すればL1 Cache Hitです。L1 CacheにはなくてもL2 Cacheで見つかれば、メインメモリより高速にデータを取得できます。
Cache Missとは
CPUが必要とするデータを対象のキャッシュ内で見つけられないことをCache Missと呼びます。
L1 CacheでCache Missが発生するとL2 Cacheを確認し、L2 CacheにもなければL3 Cacheを確認します。すべてのキャッシュで見つからなければ、最終的にメインメモリからデータを読み込みます。
メインメモリから取得したデータは、通常はキャッシュにも格納されます。そのため、同じデータを再び利用するときは、キャッシュから取得できる可能性があります。
Cache Lineとは
CPUキャッシュは、データを1バイトずつ個別に管理しているわけではありません。一定の大きさにまとめた単位で、メインメモリとキャッシュの間を転送します。この単位をCache Lineと呼びます。
現在の一般的なCPUでは、Cache Line Sizeとして64 bytesがよく利用されます。ただし、実際のサイズはCPUアーキテクチャによって異なるため、Linux上の情報から確認することが重要です。
なぜ周辺のデータもまとめて取得するのか
プログラムでは、あるメモリアドレスへアクセスした後、その近くにあるデータへ続けてアクセスすることがよくあります。そこで、必要な1バイトだけではなく、その周辺を含むCache Line単位で取得しておけば、次のアクセスをキャッシュから処理できる可能性が高くなります。
例えばCache Line Sizeが64 bytesの場合、プログラムが4 bytesの整数を1つ読み込んだとしても、キャッシュにはその整数を含む64 bytes分のデータが読み込まれます。

その後、同じCache Line内にある隣接データへアクセスすれば、メインメモリへ再度アクセスせずに処理できる可能性があります。この性質は空間的局所性(Spatial Locality)と深く関係しています。
CPUキャッシュが高速化できる理由
CPUキャッシュが有効に機能する背景には、プログラムのメモリアクセスに局所性があることが挙げられます。代表的なものが、時間的局所性と空間的局所性です。
時間的局所性
時間的局所性(Temporal Locality)とは、一度利用したデータや命令が、近い将来に再び利用される可能性が高いという性質です。
例えばループ処理では、ループを制御する変数や同じ命令が繰り返し利用されます。これらがキャッシュに保持されれば、CPUは同じデータや命令をメインメモリから何度も読み込まずに済みます。
空間的局所性
空間的局所性(Spatial Locality)とは、あるメモリアドレスを利用すると、その近くにあるデータも近い将来に利用される可能性が高いという性質です。
配列を先頭から順番に読み込む処理は、空間的局所性の代表的な例です。配列の要素は通常、メモリ上に連続して配置されるため、1つの要素を含むCache Lineを取得すると、その次の複数要素も同じCache Line内に含まれる可能性があります。
このため、配列を連続して走査する処理は、離れたアドレスをランダムに参照する処理よりもキャッシュを効率的に利用しやすくなります。
配列アクセスの順序が性能に影響する
同じ個数のデータを処理する場合でも、メモリへアクセスする順序によって性能が変わることがあります。
配列を先頭から順番に処理すると、読み込んだCache Line内のデータを続けて利用できます。一方、大きな配列内をランダムな順番で参照すると、アクセスするたびに別のCache Lineが必要となり、Cache Missが増加する可能性があります。
また、CPUには将来のメモリアクセスを予測してデータを先に読み込むHardware Prefetcherが搭載されている場合があります。連続した規則的なアクセスは予測しやすいため、この仕組みも利用しやすくなります。
ただし、実際の性能はデータサイズ、アクセスパターン、CPUアーキテクチャ、コンパイラ最適化などにも左右されます。「配列であれば必ず高速」というわけではなく、キャッシュに適したアクセスになっているかが重要です。
マルチコアCPUとCPUキャッシュ
現在のサーバーやPCでは、複数のCPUコアを搭載したマルチコアCPUが一般的です。マルチコアCPUでは、各CPUコアがすべてのキャッシュを完全に共有しているとは限りません。
一般的な構成では、L1 CacheとL2 CacheはCPUコアごとに配置され、L3 Cacheは複数のCPUコアで共有されます。
各コアが専用のL1・L2 Cacheを持つことで、それぞれのコアが必要なデータへ高速にアクセスできます。また、共有L3 Cacheを通じて、複数コアが利用するデータを効率よく保持できます。
複数のCPUコアが同じデータを利用する場合
複数のCPUコアが同じメモリ上のデータを読み書きする場合、それぞれのコアのキャッシュに同じデータのコピーが存在することがあります。
例えばCore 0があるデータを変更したにもかかわらず、Core 1が古いキャッシュ上の値を使い続けると、コアごとに見えるデータが一致しなくなります。
この問題を防ぎ、複数のキャッシュ間でデータの整合性を保つ仕組みをCache Coherencyと呼びます。CPUはキャッシュ間で情報をやり取りし、あるコアがデータを更新した場合に、他のコアが古いデータをそのまま利用しないよう制御します。
Cache Coherencyを実現する代表的なプロトコルとしてMESIなどがありますが、ここでは「複数コアのキャッシュ内容を矛盾させないための仕組み」と理解しておけばよいでしょう。
CPUキャッシュが性能に与える影響
CPUキャッシュは、アプリケーション性能に大きな影響を与えます。CPU使用率が高いか低いかだけではなく、CPUが必要なデータをどこから取得しているかも重要です。

Cache Hitが多い場合
Cache Hitが多い場合、CPUは必要なデータをL1・L2・L3 Cacheから取得できます。メインメモリへのアクセスを減らせるため、データ待ちの時間が短くなり、命令を効率よく実行できます。
Cache Missが多い場合
Cache Missが増えると、下位のキャッシュやメインメモリへのアクセスが増加します。特に最終レベルのキャッシュでもデータが見つからない場合、CPUはメインメモリからデータが届くのを待つ必要があります。
必要なデータを待つことでCPU内部の処理が進みにくくなる状態を、一般にCPU Stallと呼びます。CPU使用率が高い場合でも、CPU内部ではメモリアクセス待ちによるStallが多く発生し、1サイクル当たりに実行できる命令数が低下している可能性があります。
大きなデータをランダムに参照する処理、キャッシュ容量を大きく超えるデータを繰り返し処理する場合、複数コアが同じCache Lineを頻繁に更新する場合などでは、キャッシュ効率が低下する可能性があります。
したがって、アプリケーション性能を調査するときは、CPU使用率だけでなく、Cache Missやメモリアクセスの状況も合わせて確認することが重要です。
LinuxでCPUキャッシュを確認する
Linuxでは、lscpuやsysfsからCPUキャッシュの構成を確認できます。CPUによってキャッシュ構成は異なるため、実際の環境で確認してみましょう。
lscpuで確認する
lscpuは、CPUアーキテクチャ、CPU数、Core数、NUMA構成、キャッシュ情報などを表示するコマンドです。
出力結果には、下記のようなCPUキャッシュ情報が記載されます。

| 項目 | 意味 |
|---|---|
L1d | L1 Data Cache |
L1i | L1 Instruction Cache |
L2 | L2 Cache |
L3 | L3 Cache |
なお、lscpuのバージョンや環境によっては、表示される容量が1つのキャッシュ当たりではなく、同じレベルのキャッシュを合計した値になっている場合があります。インスタンス数なども合わせて確認してください。
sysfsで確認する
CPUごとの詳細なキャッシュ情報は、sysfsの/sys/devices/system/cpu/配下から確認できます。CPU0のキャッシュ情報を確認する場合は、次のディレクトリを参照します。index0、index1、index2、index3などのディレクトリは、それぞれが特定のキャッシュを表します。

各キャッシュに関する情報は、次のコマンドで確認できます。
grep . /sys/devices/system/cpu/cpu0/cache/indexX/{level,type,size,coherency_line_size,shared_cpu_list}

| ファイル | 確認できる内容 |
|---|---|
level | L1・L2・L3などのキャッシュレベル |
type | Data、Instruction、Unifiedなどの種類 |
size | キャッシュ容量 |
coherency_line_size | Cache Line Size |
shared_cpu_list | キャッシュを共有する論理CPU |
例えばcoherency_line_sizeが64であれば、そのキャッシュのCache Line Sizeは64 bytesです。また、L3 Cacheのshared_cpu_listに複数のCPU番号が表示されていれば、その論理CPU間でキャッシュが共有されていることを確認できます。
CPUキャッシュを意識した性能調査
Linuxの性能調査では、最初にtopやmpstatを使い、CPU使用率を確認することがよくあります。しかし、CPU使用率だけでは、CPU内部で命令がどれだけ効率よく実行されているのかまでは分かりません。

例えばアプリケーションの処理対象データが大きくなり、CPUキャッシュへ収まりにくくなると、Cache Missが増える可能性があります。Cache Missによってメインメモリへのアクセスが増えると、CPUはデータ待ちに多くの時間を使います。
この場合、CPU使用率だけを見るとCPUが忙しく動作しているように見えることがあります。しかし、実際にはメモリアクセス待ちによってCPU Stall※が増加し、単位時間当たりに完了できる処理が減っている可能性があります。
※CPU Stallとは、CPU Stallとは、データ待ちや命令間の依存関係などにより、CPU内部の命令実行パイプラインを十分に進められない状態です。メモリアクセス待ち、命令間のデータ依存、分岐予測ミス、実行ユニットなどCPU内部リソースの競合によって本状態になります。
CPU使用率とCache Missを組み合わせて確認する
性能問題を調査するときは、まずtopやmpstatからCPU全体やCPUごとの使用率を確認します。そのうえで、CPU負荷の上昇とアプリケーション性能の低下が同時に発生している場合は、環境によってはperf statを使って命令数、CPUサイクル数、Cache Missなどを確認します。
perf stat -e cycles,instructions,cache-references,cache-misses <command>cyclesとinstructionsを合わせて確認すると、CPUサイクルに対してどの程度命令を実行できたかを見る手掛かりになります。ただし、仮想サーバなどではPMU(Performance Monitoring Unit)が有効になっておらず、上記情報が確認できないことがありますのでご留意ください。
まとめ
CPUキャッシュは、CPUとメインメモリの速度差を補い、CPUが必要なデータへ高速にアクセスするための重要な仕組みです。
L1、L2、L3 Cacheは速度や容量が異なり、CPUはこれらのキャッシュを利用することでメインメモリへのアクセスを減らしています。また、Cache Lineや時間的局所性、空間的局所性を理解することで、CPUキャッシュがどのように性能向上につながるのか理解しやすくなります。
性能問題を調査する際には、CPU使用率だけでなく、Cache MissやCPU Stallにも注目することが重要です。Linuxではlscpuやsysfsでキャッシュ構成を確認でき、環境によってはperfを利用してCache Missなどを確認できます。


コメント