許多 Java 開發者以為 CPU 快取屬於 C/C++ 領域,直到遇到 LMAX Disruptor、Martin Fowler 文章才發現:資料在記憶體中的佈局直接決定 Java 程式效能。本文聚焦 CPU 快取對 Java 的影響,不涉及硬體電路細節。
| 層級 | 容量 | 速度 | 範圍 |
|---|---|---|---|
| L1 | 最小 (32-64 KB) | 最快 (~3-4 cycles, 0.5-1 ns) | 每核心獨有(分 L1d 數據 / L1i 指令) |
| L2 | 中等 (256 KB - 1 MB) | 次之 (~10-20 cycles, 3-7 ns) | 每核心獨有 |
| L3 | 最大 (8-32 MB) | 最慢 (~40-45 cycles, 15 ns) | 同插槽核心共享 |
| 主記憶體 | GB 級 | ~120-240 cycles (60-120 ns) | 全系統共享 |
CPU 依序尋找 L1→L2→L3→記憶體,路徑越長延遲越高。目標:讓熱點資料盡可能停留在 L1。
cpuid 或 /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 查看)。long (8 bytes) 會順帶把相鄰 56 bytes 載入同一緩存行。long[1024*1024][6] → 每列 16 bytes (物件頭) + 6×8 = 64 bytes = 剛好一條緩存行
// 快:行優先(符合記憶體佈局)
for (int i = 0; i < DIM1; i++)
for (int j = 0; j < DIM2; j++)
sum += arr[i][j];
// 慢:列優先(踩地雷)
for (int j = 0; j < DIM2; j++)
for (int i = 0; i < DIM1; i++)
sum += arr[i][j];
原因:列優先每次存取不同列同欄,導致剛載入的緩存行(含 6 個 long)僅用到 1 個就被擠出,造成大量 L1 Miss。
Prefetch、資料結構預熱)。@Contended、手動填充 long[7])、ThreadLocal、Disruptor RingBuffer 設計。LongAdder(分段計數減少緩存行競爭)而非 AtomicLong。StampedLock 樂觀讀或 CopyOnWriteArrayList。-XX:+PrintCompilation -XX:+PrintInlining 觀察 JIT 行為,結合 perf、VTune 分析硬體計數器。