- Published on
CPU Cache & メモリ階層 完全ガイド 2025: L1/L2/L3, Cache Line, False Sharing, 性能最適化
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR
- メモリ階層 = 性能の本質: L1 Cache Miss は 100 倍遅い
- Cache Line (64 bytes): メモリアクセスの単位
- False Sharing: マルチスレッド性能のキラー
- Cache-friendly データ構造: Array > LinkedList、SoA > AoS
- Mechanical Sympathy: ハードウェアを理解すれば 100 倍速い
1. メモリ階層
1.1 なぜ階層が必要か
理想: すべてのメモリが速く、大きく、安ければよいが...
現実のトレードオフ:
- 速い = 小さい = 高価
- 遅い = 大きい = 安価
階層構造でバランスをとる。
1.2 メモリ階層 (2025)
| 段 | サイズ | レイテンシ | 備考 |
|---|---|---|---|
| Register | ~32 個 x 64bit | 1 cycle | CPU コア内 |
| L1 cache | 32-64 KB | 4 cycles (~1ns) | コアごと |
| L2 cache | 256 KB-1 MB | 10 cycles (~3ns) | コアごと or 共有 |
| L3 cache (LLC) | 4-128 MB | 40 cycles (~12ns) | 全コア共有 |
| RAM (DDR5) | 16-512 GB | 100 cycles (~30ns) | システム全体 |
| NVMe SSD | 1-100 TB | 100 microseconds | 永続化 |
| HDD | 1-20 TB | 10 milliseconds | ほぼ未使用 |
1.3 スケール感
L1 を 1 秒とすると:
- L2 = 3 秒
- L3 = 12 秒
- RAM = 30 秒
- SSD = 27 時間
- HDD = 115 日
メモリは L1 より約 100 倍遅い。ディスクは 100 万倍遅い。
1.4 意味
メモリアクセス = コスト
Cache Hit = 安い
Cache Miss = 高い
良いコード = Cache Hit が多いコード。
2. CPU Cache の仕組み
2.1 Cache Line
Cache Line は Cache の最小単位、通常 64 bytes。
RAM から 1 byte 読む?
-> 64 bytes の Cache Line 単位で取得
効果:
- 近傍メモリが一緒にキャッシュされる
- 局所性 (Locality) の自然な活用
2.2 2 種類の局所性
Temporal Locality: 直近アクセスしたものは再アクセスされやすい (ループ変数)。 Spatial Locality: 近傍メモリはすぐアクセスされる (配列走査)。
良いコードは両方を活用する。
2.3 Cache Mapping
- Direct Mapped: 各アドレスは 1 本の Cache Line のみ
- Set Associative: 複数本から選択 (8-way が一般的)
- Fully Associative: どこでも可 (実装困難)
現代の CPU は 8-way Set Associative が主流。
2.4 Cache 置換
- LRU (Least Recently Used): 最も古い
- Pseudo-LRU: 近似 (実装が容易)
- Random: 意外にも LRU と大差ない
3. Cache Miss の種類
3.1 Compulsory Miss (Cold Miss)
初回アクセス。避けられない。
緩和: Prefetch。
for (int i = 0; i < n; i++) {
process(arr[i]);
// CPU が arr[i+1], arr[i+2] を先読み
}
3.2 Capacity Miss
Cache が小さすぎて発生。
対策: ワーキングセットを Cache に収める、Cache Blocking (Tiling)。
// Blocking なし
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
C[i][j] += A[i][k] * B[k][j];
// Blocking あり
for (int ii = 0; ii < n; ii += BLOCK)
for (int jj = 0; jj < n; jj += BLOCK)
for (int kk = 0; kk < n; kk += BLOCK)
for (int i = ii; i < ii + BLOCK; i++)
for (int j = jj; j < jj + BLOCK; j++)
for (int k = kk; k < kk + BLOCK; k++)
C[i][j] += A[i][k] * B[k][j];
10 倍以上高速化することもある。
3.3 Conflict Miss
複数データが同じ Cache Line スロットに写像。Padding やアライメントで対処。
4. Cache-friendly データ構造
4.1 Array vs LinkedList
// Array - Cache friendly
int arr[1000];
for (int i = 0; i < 1000; i++)
sum += arr[i]; // 連続メモリ、Prefetch が効く
// LinkedList - Cache hostile
struct Node {
int value;
struct Node* next;
};
Node* curr = head;
while (curr) {
sum += curr->value;
curr = curr->next; // 場所不明、Cache Miss
}
ベンチマーク: 大規模データで Array は LinkedList より 10-100 倍高速。
4.2 AoS vs SoA
AoS (Array of Structures):
struct Point {
float x, y, z;
int color;
};
Point points[1000];
for (int i = 0; i < 1000; i++)
sum += points[i].x;
// 毎回 16 bytes を取得、使うのは 4 bytes のみ
SoA (Structure of Arrays):
struct Points {
float x[1000];
float y[1000];
float z[1000];
int color[1000];
};
for (int i = 0; i < 1000; i++)
sum += points.x[i];
// 4 bytes 取得、100% 活用、SIMD も容易
効果: 4-10 倍高速。ゲームエンジン (ECS)、データ分析、ML で使用。
4.3 Struct Padding とアライメント
struct Bad {
char a; // 1 byte + 7 padding
double b; // 8 bytes
char c; // 1 byte + 7 padding
};
// sizeof(Bad) = 24 bytes (16 無駄!)
struct Good {
double b; // 8 bytes
char a; // 1 byte
char c; // 1 byte + 6 padding
};
// sizeof(Good) = 16 bytes
ルール: 大きいフィールドを先に。
4.4 Hash Map の Cache 特性
- Open Addressing (Robin Hood, Swiss Table): 1 配列に集約、Cache friendly。
- Separate Chaining (伝統的): 衝突時 LinkedList、Cache hostile。
Google の Swiss Table は SIMD で 16 スロットを同時検索。
5. False Sharing
5.1 問題
struct Counters {
atomic<int> a; // 4 bytes
atomic<int> b; // 4 bytes
};
// 同じ Cache Line (64 bytes) に配置される
Thread 1 が a を書き、Thread 2 が b を書く。独立だが 同じ Cache Line のため相互無効化、無限 Ping-Pong。
性能低下: 10-100 倍。
5.2 検出
perf c2c record ./my_program
perf c2c report
5.3 対処
Padding:
struct Counters {
alignas(64) atomic<int> a;
alignas(64) atomic<int> b;
};
C++17 hardware_destructive_interference_size:
#include <new>
struct Counters {
alignas(std::hardware_destructive_interference_size) atomic<int> a;
alignas(std::hardware_destructive_interference_size) atomic<int> b;
};
Thread-local:
thread_local int a;
thread_local int b;
5.4 LongAdder (Java)
// AtomicLong - False Sharing 発生しうる
AtomicLong counter = new AtomicLong();
counter.incrementAndGet();
// LongAdder - セルに分散して False Sharing 回避
LongAdder counter = new LongAdder();
counter.increment();
内部で Padding を利用。16+ スレッドで 100 倍以上高速。
6. MESI Protocol
6.1 Cache Coherence
マルチコアで同じ Cache Line を持つと一貫性問題が発生。MESI は 4 状態を管理:
- M (Modified): dirty、このコアのみ
- E (Exclusive): clean、このコアのみ
- S (Shared): clean、複数コア
- I (Invalid): 無効
6.2 状態遷移
[I] -> read -> [E] (単独) or [S] (共有)
[E] -> write -> [M]
[S] -> write -> [M] (他コアに invalidate)
[M] -> 他コア read -> [S] + writeback
MOESI、MESIF などの派生もある。Coherence Traffic こそ False Sharing のコスト源。
7. NUMA — Non-Uniform Memory Access
7.1 NUMA とは
大規模サーバ (40+ コア) は複数 Socket を持ち、各 Socket に自分のメモリバンクがある。
[Socket 1] [Socket 2]
| |
[Memory 1] [Memory 2]
| |
+--- Bus -------+
- ローカルメモリ: ~100ns
- リモートメモリ: ~200ns
7.2 影響
配置ミスで 50% の性能劣化。
7.3 NUMA-aware プログラミング
numactl --cpunodebind=0 --membind=0 ./my_program
numactl --hardware
#include <numa.h>
void* mem = numa_alloc_local(size);
7.4 ユースケース
- Databases (PostgreSQL, MySQL): NUMA-aware 設定は必須
- JVM:
-XX:+UseNUMA - Redis: 単一スレッドのため NUMA 感度が高い
- Kubernetes: NUMA Topology Manager
8. Mechanical Sympathy
8.1 概念
Martin Thompson (LMAX Disruptor 創始者) が強調:
"機械がどう動くかを理解すれば、最適なコードが書ける。"
レーサーがエンジンを理解するように、プログラマも CPU を理解すべき。
8.2 LMAX Disruptor
- 課題: 毎秒 600 万件の金融取引
- 解: Mechanical Sympathy を極限まで追求
- 手法: Ring Buffer (配列ベース、Cache friendly)、False Sharing 回避 (Padding)、Single-writer 原則 (ロックフリー)、Pre-allocation (GC 回避)
- 結果: Java で C++ 相当の性能
8.3 7 原則
- Single Writer Principle — ロック不要
- Write Combining
- Pad to avoid False Sharing
- Immutable オブジェクト
- GC Pause 回避 (pre-allocate)
- Cache-friendly データ構造
- ハードウェアを理解する
8.4 例
// Bad - False Sharing
class BadCounter {
long count;
}
// Good - Padding
class GoodCounter {
long p1, p2, p3, p4, p5, p6, p7;
long count;
long p9, p10, p11, p12, p13, p14, p15;
}
@Contended アノテーション (Java 8+):
@sun.misc.Contended
class Counter {
long count;
}
9. 計測と Profiling
9.1 perf
perf stat -e cache-misses,cache-references ./my_program
9.2 L1 Miss Rate
perf stat -e L1-dcache-load-misses,L1-dcache-loads ./my_program
L1 Miss Rate が 5% を超えるなら最適化の余地あり。
9.3 Intel VTune
商用 (無料プランあり)。メモリアクセスパターン、False Sharing 検出、SIMD 活用度を分析。
9.4 cachegrind (Valgrind)
valgrind --tool=cachegrind ./my_program
cg_annotate cachegrind.out.PID
関数ごとの Cache Miss レポート。
9.5 Microbenchmark
Google Benchmark (C++):
static void BM_Array(benchmark::State& state) {
int arr[1000];
for (auto _ : state) {
long sum = 0;
for (int i = 0; i < 1000; i++)
sum += arr[i];
benchmark::DoNotOptimize(sum);
}
}
BENCHMARK(BM_Array);
JMH (Java) も同等機能。
10. 実戦最適化
10.1 Matrix Multiplication
Naive:
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
for (k = 0; k < N; k++)
C[i][j] += A[i][k] * B[k][j];
B[k][j] が Cache 非親和的。
Loop Swap:
for (i = 0; i < N; i++)
for (k = 0; k < N; k++)
for (j = 0; j < N; j++)
C[i][j] += A[i][k] * B[k][j];
Tiling:
for (ii = 0; ii < N; ii += T)
for (kk = 0; kk < N; kk += T)
for (jj = 0; jj < N; jj += T)
for (i = ii; i < ii+T; i++)
for (k = kk; k < kk+T; k++)
for (j = jj; j < jj+T; j++)
C[i][j] += A[i][k] * B[k][j];
SIMD + Threading (BLAS) を加えると Naive の 100 倍以上。
10.2 Hash Table 比較
std::unordered_map: Separate Chaining、Cache 非親和的- Swiss Table / dense_hash_map: Open Addressing + SIMD、2-5 倍高速
- Robin Hood hashing: もう一つの Cache-friendly 派生
10.3 ゲームエンジン ECS
従来の OOP は Cache に巨大なオブジェクトを乗せる。ECS はコンポーネントごとの配列に分割し、必要な配列だけを走査する。Unity DOTS、Unreal Mass で 10 倍以上高速化。
10.4 Columnar Database
- Row 指向 (PostgreSQL, MySQL): 1 カラムでも全行を読む
- Column 指向 (ClickHouse, Druid): 必要なカラムのみ読む
SELECT AVG(age) のようなクエリで Columnar は 100 倍以上高速。
クイズ
1. L1 Cache と RAM のアクセス時間差は?
答え: L1 は ~1ns (4 cycles)、RAM は ~30ns (100 cycles)、約 30 倍差。L1 を 1 秒に例えると RAM は 30 秒、SSD は 27 時間。階層の各段が 10 倍ずつ遅い。だから Cache-friendly コードは 100 倍速い。単なる最適化ではなく本質的な差。
2. False Sharing とは何か、どう防ぐか?
答え: 2 変数が同じ 64 bytes Cache Line に乗ると、スレッドがどちらかに書くたびに他スレッドの Cache が無効化され Ping-Pong 発生、10-100 倍の低下。対策は (1) Padding (alignas(64))、(2) C++17 hardware_destructive_interference_size、(3) thread_local、(4) Java @Contended。検出は perf c2c。
3. AoS と SoA の違いは?
答え: Array of Structures は 1 レコードをまとめて格納。Structure of Arrays はフィールドごとに別配列。SoA は特定フィールドのみ走査するとき Cache 利用率 100% で SIMD も可能、4-10 倍高速。Unity DOTS、Unreal Mass、データ分析、ML で採用。
4. マルチ Socket サーバで NUMA が重要な理由は?
答え: 各 Socket が専用メモリを持ち、ローカル ~100ns、リモート ~200ns。配置ミスで 50% 低下。numactl バインド、numa_alloc_local、JVM -XX:+UseNUMA で対処。PostgreSQL、MySQL、Redis のチューニングに必須。
5. Mechanical Sympathy と LMAX Disruptor の秘訣は?
答え: Martin Thompson の言葉で「機械の動作を理解すれば最適コードが書ける」。Disruptor は Java で 600 万 tx/sec: Ring Buffer (配列、Cache friendly)、Single-writer 原則 (ロックフリー)、Padding (False Sharing 回避)、Pre-allocation (GC 回避)、Immutable。結果、Java で C++ 級性能。