Appearance
cache-miss —— 缓存未命中模式实验
通过 3 种数组遍历模式(顺序/随机/跨步),用
perf stat定量测量 L1/LLC 缓存命中率的差异。
1. 背景与设计
1.1 什么是缓存未命中
CPU 访问内存前先查缓存(L1 → L2 → LLC → DRAM),每级未命中就降一级。
| 缓存层级 | 典型大小 | 延迟 | 说明 |
|---|---|---|---|
| L1 | 32 KB | 4~5 cycles | 每核私有,最快 |
| L2 | 256 KB~1 MB | 12~14 cycles | 每核私有 |
| LLC (L3) | 10~30 MB | 40~60 cycles | 所有核共享,最后一道缓存防线 |
| DRAM | — | 100~300 cycles | LLC miss 才会到 |
遍历模式决定了缓存能"猜中"多少后续访问——这就是"缓存友好"的含义。
1.2 实验全景
物理实体与关系

- Core → L1 → L2 → LLC → 内存控制器 → DRAM:数据访问的物理路径,越往右延迟越高(4 cycles → 100+ cycles)
- 硬件预取器:观察访问地址序列,发现规律(连续/固定步长)后提前把后续 cache line 抓进 L1。顺序模式受益,随机模式无效
- perf / PMU:CPU 内置的硬件计数器,非侵入式记录各级缓存的 load/miss 事件,不影响程序执行
- 64 MB 数组:故意选得比 LLC(~30 MB)大,确保无法全塞进缓存
访问流程(顺序 vs 随机)

一句话概括
程序分配 64 MB 数组 → CPU 按指定模式遍历赋值(顺序/随机/跨步)→ 每次访问触发缓存层级查找(L1 → L2 → LLC → DRAM)→ 硬件预取器根据访问模式决定是否提前缓存→ perf PMU 无侵入计数各层级事件 → 对比不同模式下的吞吐和 cache miss 数据,量化"缓存友好"有多值钱。
1.3 设计思路
核心思路:控制计算量不变,只改变内存访问模式,观察性能差异。
| 设计决策 | 做法 | 目的 |
|---|---|---|
| 数组大小 | N = 16,777,216 int = 64 MB | 远超典型 LLC(~30 MB),确保数据不可能全部塞进缓存 |
| 计算量 | 所有模式统一 N/2 = 8,388,608 次 a[...]++ | 公平对比,性能差异只能来自访存模式 |
| 3 种遍历 | 顺序 a[i] / 随机 a[rand_idx] / 跨步 a[i*stride] | 覆盖缓存友好→不友好的完整梯度 |
| 随机索引预生成 | std::mt19937 生成好索引再计时 | 排除 rand() 本身计入测试耗时 |
| 编译优化 | 默认 -O2 | 效率差异源于访存模式而非编译;volatile sink 防止自增被优化掉 |
| 吞吐量单位 | 亿次自增/秒 | 越大越好,且与 CPU 频率解耦 |
1.4 设计预期
基于缓存层级(L1 32 KB、cache line 64 B = 16 int),推测各模式的预期行为:
| 模式 | 每次跨 line | L1 行为 | 预取器 | 预期吞吐 | 关键依据 |
|---|---|---|---|---|---|
| 顺序 | 0 | 连续 16 次访问同一条 line | 完美工作 | 100% | stream prefetcher 识别连续地址 |
| stride=2 | 0 | 同 line 内 8 次后跳下一条 | 部分工作 | 70~80% | 仍在 line 内,触发频率减半 |
| stride=8 | 0 | 同 line 内 2 次 | 可能识别 | 40~60% | 不跨 line,但 stride pattern 未必生效 |
| stride=16 | 1 | 每次恰好跨一条 line | 可能失效 | 10~20% | 64/4=16,恰好落 line 边界 |
| stride=64 | 4 | 每次跨 4 条 line,基本全 miss | 完全失效 | 5~10% | stride ≥ line size,无连续地址可识别 |
| stride=256 | 16 | 全 miss | 完全失效 | 5~10% | 同上 |
| stride=1024 | 64 | 全 miss | 完全失效 | 5~10% | 同上 |
| 随机 | 不可预测 | 全 miss,无规律 | 完全失效 | <10% | 每次大概率不同 line,无法预取 |
核心预期:(1)顺序 vs 随机差 5~15×;(2)stride 从小到大阶梯下降:<16 高 → 16 断崖 → ≥64 底;(3)随机 LLC-loads 是顺序的数百倍,IPC 跌到 <1。
1.5 程序结构
| 模式 | 命令 | 访问方式 |
|---|---|---|
| 顺序 | ./cache_miss seq | a[i]++ |
| 随机 | ./cache_miss rand | a[预生成的随机索引]++ |
| 跨步 | ./cache_miss stride N | a[(i*N)%N]++ |
| 全量对比 | ./cache_miss(无参数) | 依次跑以上 8 种并打印对比表 |
数组 64 MB > 典型 LLC(~30 MB),确保各级缓存行为都被暴露。
2. 快速开始
2.1 编译与运行
bash
make # 编译(-O2)
make run # 跑全部模式对比表(= ./cache_miss)
make clean # 清理
# 单独跑某个模式
./cache_miss seq # 顺序访问
./cache_miss rand # 随机访问
./cache_miss stride 64 # stride=64 跨步2.2 perf 测量命令
bash
# 基础:cycles + IPC + 通用 cache 事件
perf stat -e cycles,instructions,cache-references,cache-misses ./cache_miss seq
perf stat -e cycles,instructions,cache-references,cache-misses ./cache_miss rand
# L1 数据缓存
perf stat -e L1-dcache-loads,L1-dcache-load-misses ./cache_miss seq
perf stat -e L1-dcache-loads,L1-dcache-load-misses ./cache_miss rand
# 最后一级缓存 (LLC)
perf stat -e LLC-loads,LLC-load-misses ./cache_miss seq
perf stat -e LLC-loads,LLC-load-misses ./cache_miss rand
# 采样热点 — 哪行代码触发了 cache miss
perf record -e cache-misses -g ./cache_miss rand
perf report --stdio
# 一键全跑
make perf-all3. 指标速查
每个指标的含义和"数值高/低代表什么",避免阅读实测数据时来回查定义。
3.1 执行效率类
| 指标 | 含义 | 怎么判断 |
|---|---|---|
| elapsed | 端到端耗时(秒) | 越低越好 |
| cycles | CPU 总时钟周期数 | 同样计算量下越低越好 |
| instructions | 执行的总指令数 | 绝对值无意义,和 cycles 配合看 |
| IPC | instructions / cycles | 越高越好:峰值 4~5,负载 1~3 常见,<1 说明 CPU 大量时间在等内存/IO |
3.2 缓存事件类(perf 通用事件)
| 指标 | 含义 | 怎么判断 |
|---|---|---|
| cache-references | 任何缓存访问的总次数(含取指、预取、RFO) | 作为 cache-misses 的分母 |
| cache-misses | 任何缓存未命中的总次数 | 越低越好 |
| cache-miss 率 | cache-misses / cache-references | 越低越好 |
3.3 L1 数据缓存事件
| 指标 | 含义 | 怎么判断 |
|---|---|---|
| L1-dcache-loads | L1 数据 load 次数 | 低说明预取器在工作(减少了显式 load) |
| L1-dcache-load-misses | L1 load 未命中次数 | 越低越好 |
| L1 miss 率 | misses / loads | 越低越好,但分母被预取压缩后可能虚高 |
3.4 LLC 事件
| 指标 | 含义 | 怎么判断 |
|---|---|---|
| LLC-loads | 到达 LLC(L3)的次数 | 越低越好——说明 L1/L2 就命中了 |
| LLC-load-misses | LLC 也未命中、必须去 DRAM | 越低越好(意味着等 DRAM 100~300 cycles) |
| LLC miss 率 | misses / loads | >50% 说明大量时间在等内存 |
3.5 命名规律
*-loads= 该级 load 总次数,*-load-misses= 未命中次数*-miss 率=misses / references(或misses / loads)- 通用事件(
cache-*)与 PMU 精确事件(L1-dcache-*/LLC-*)是两组不同来源的计数器,不能交叉相除
4. 实测数据(Intel 至强)
数组 N = 16,777,216 int = 64 MB,每组 OPS = 8,388,608 次自增。 数据来源:
make run+perf stat。
4.1 吞吐量总览
bash
╔══════════════════════════════════════════════════════════╗
║ 缓存未命中模式对比实验 ║
║ 数组 N=16777216 ints = 64MB (>典型LLC) ║
║ 每组操作 = 8388608 次自增 ║
╚══════════════════════════════════════════════════════════╝
模式 吞吐(亿次/s) 相对(%)
----------------------------------------
顺序访问 1.203 100.0 %
stride=2 0.866 72.0 %
stride=8 0.254 21.1 %
stride=16 0.129 10.8 %
stride=64 0.085 7.1 %
stride=256 0.096 8.0 %
stride=1024 0.090 7.5 %
随机访问 0.103 8.5 %三档规律:
| 档位 | 模式 | 相对吞吐 | 主导因素 |
|---|---|---|---|
| 缓存友好 | 顺序、stride=2 | 100% / 72% | 预取器完美,L1 命中 |
| 过渡区 | stride=8、16 | 21% / 11% | 预取器半失效,line 边界跨越 |
| 缓存失效 | stride≥64、随机 | 7~8% | 每次访问穿透到 LLC/DRAM |
最关键的数字:顺序 vs 随机差 11.7×——同一份计算量,只改遍历顺序,性能就差一个数量级。
4.2 吞吐趋势分析
档位之间为什么掉:
- 顺序→stride=2:掉到 72%,每个 cache line(64 B = 16 int)被访问 8 次(而非 16 次),触发的 line 数减半。
- stride=2→stride=16:从 72% 陡降到 10.8%。stride=16 仍在 cache line 内(16×4=64 B = 1 line),但恰好落在 line 边界上,预取器 stride pattern 识别失败。
- stride≥64:稳定在 7~8%,与随机同一水平。每次访问都触发新 cache line,预取器完全失效。
4.3 perf 计数器对比(顺序 vs 随机)
分 3 次运行
perf stat—— 每次只测一类事件,避免 PMU 计数器溢出和相互干扰。 注意同一进程在 3 次运行下"吞吐"略有差异:顺序模式 1.263 / 1.369 / 1.625 亿次/秒(差 ~28%),来自运行间方差(CPU 频率抖动、缓存冷热、kernel 调度);随机模式 0.101 / 0.102 / 0.101 亿次/秒,方差极小(被 DRAM 等待主导)。下面的数字就是这次实际跑出的,没有抹平。
4.3.1 CPU 基础效率 + 通用缓存事件
perf stat -e cycles,instructions,cache-references,cache-misses,一次测量拿到 7 个指标。
| 指标 | 顺序 | 随机 | 倍 | 解读 |
|---|---|---|---|---|
| elapsed | 0.0458 s | 0.2975 s | 6.5× | 墙钟耗时。6.5× 远小于 cycles 的 44×,因为 wall time 还包含了内核调度等开销 |
| cycles | 23.72 M | 1,053.66 M | 44× | 同样计算量下 CPU 多用 44 倍周期,全是等内存浪费的 |
| instructions | 43.94 M | 699.16 M | 16× | 随机多了 15× 回退/重试/stall 指令 |
| IPC | 1.85 | 0.66 | 0.36× | <1 = 过半周期在空转等内存。0.66 意味着只有 ~1/3 周期在干活 |
| cache-references | 1.56 M | 10.85 M | 7× | 通用缓存访问总次数(含指令取指、预取、RFO) |
| cache-misses | 0.58 M | 7.58 M | 13× | 通用缓存未命中总次数 |
| cache-miss 率 | 37.25% | 69.89% | 1.9× | 随机模式下约 70% 访问未命中 |
- 核心差距:
cycles差 44× 是全部指标里差距最大的——说明真正的性能杀手不是指令多了多少(16×),而是 每次 cache miss 等 DRAM 的时间积累。 - 顺序 mode 的 cache-miss 率 37% 也不低,原因是数组 64 MB >> L1 32 KB,冷启动 miss + 循环后期驱逐了前半段 line。
4.3.2 L1 数据缓存事件
perf stat -e L1-dcache-loads,L1-dcache-load-misses,单独测量 L1 层。
| 指标 | 顺序 | 随机 | 倍 | 解读 |
|---|---|---|---|---|
| elapsed | 0.0245 s | 0.3019 s | 12.3× | 本组 elapsed 与 §4.3.1 不同,来自不同 perf 运行 |
| L1-dcache-loads | 9.08 M | 135.07 M | 15× | 这也是"预取器是否工作"的直接证据 |
| L1-dcache-load-misses | 1.57 M | 10.98 M | 7× | 绝对 miss 数差 7×,比 miss 率更有意义 |
| L1 miss 率 | 17.35% | 8.13% | 0.47× | 反常:顺序 > 随机,原因见下 |
- L1-loads 差 15× 才是真实故事:顺序模式预取器提前灌入 line,显式 load 被压到 9M;随机模式每次都是显式 load,膨胀到 135M。
- L1 miss 率"反常识"(17% vs 8%):分母差异(9M vs 135M)摊薄了随机模式的比率,但绝对 miss 数(1.57M vs 10.98M,差 7×)才反映真相。详见 §4.4 第 2 条。
4.3.3 LLC 事件
perf stat -e LLC-loads,LLC-load-misses,单独测量最后一级缓存。
| 指标 | 顺序 | 随机 | 倍 | 解读 |
|---|---|---|---|---|
| elapsed | 0.0402 s | 0.2792 s | 6.9× | 本组 elapsed 与上两组不同,来自不同 perf 运行 |
| LLC-loads | 9,894 (9.9 K) | 8,459,139 (8.46 M) | 855× | 反映预取器是否把流量挡在 L1 |
| LLC-load-misses | 4,954 (4.95 K) | 6,834,262 (6.83 M) | 1379× | 必须去 DRAM 的次数 |
| LLC miss 率 | 50.07% | 80.79% | 1.6× | 顺序一半命中(回流数据),随机 80% 穿透到 DRAM |
- LLC-loads 差 855× 是最大鸿沟:顺序仅 ~10K 次穿透到 LLC(stream prefetcher 直接把 line 灌进 L1);随机 8.46M 次每次都打到 LLC。
- LLC miss 率 50% vs 81%:顺序那 50% 命中说明数组小段仍在 L3;随机 80% 必须走到 DRAM,每次等待 100~300 cycles。
- LLC-load-misses 的 1379× 差距直接解释了 cycles 的 44× 差距——每次 LLC miss = ~200 cycles,1379× 的倍数是 44× 的约 31 倍,说明 LLC miss 的累积代价换算了 cycles 的暴涨。
4.3.4 综合汇总
以上 3 次独立
perf stat的合并对比(数字四舍五入到 3 位有效数字):
| 指标 | 顺序 | 随机 | 倍(随/顺) |
|---|---|---|---|
| elapsed | 0.046 s | 0.298 s | 6.5× |
| cycles | 23.7 M | 1,053.7 M | 44× |
| instructions | 43.9 M | 699.2 M | 16× |
| IPC | 1.85 | 0.66 | 0.36× |
| cache-references | 1.56 M | 10.85 M | 7× |
| cache-misses | 0.58 M | 7.58 M | 13× |
| cache-miss 率 | 37.25% | 69.89% | 1.9× |
| L1-dcache-loads | 9.08 M | 135.07 M | 15× |
| L1-dcache-load-misses | 1.57 M | 10.98 M | 7× |
| L1 miss 率 | 17.35% | 8.13% | 0.47× |
| LLC-loads | 9.9 K | 8.46 M | 855× |
| LLC-load-misses | 4.95 K | 6.83 M | 1379× |
| LLC miss 率 | 50.07% | 80.79% | 1.6× |
stride=2/8/16/64 的 perf 计数器数据待补充(本节先聚焦顺序 vs 随机这一组最大反差)
4.4 数据解读
真实瓶颈是 cycles,不是 instructions 随机比顺序多了 16× 指令(699M vs 43.9M),但耗了 44× cycles(1054M vs 24M)——多出的 28× cycles 全在等内存。IPC 从 1.85 跌到 0.66,CPU 超过一半周期在空转。
L1 miss 率"反常":顺序 17% > 随机 8%
- 顺序 L1-loads 只有 9M(预取器提前抓,减少了显式 load),但数组 64 MB >> L1 32 KB,前半段预取的 line 在循环后期被驱逐,miss 率被冷启动拉高。
- 随机 L1-loads 高达 135M(预取器不工作,每次都是显式 load),miss 绝对值 10.98M 远高于顺序 1.57M,但分母 135M 摊薄了比率。
- 绝对 miss 数(1.57M vs 10.98M,差 7×)才是真凶,miss 率会骗人。
LLC 数据是最大鸿沟
- LLC-loads 差 855×(9.9K vs 8.46M)——最能反映"预取器是否工作"。顺序仅 ~10K 次到 LLC,stream prefetcher 把后续 line 直接灌进 L1;随机每次访问都穿透到 LLC。
- LLC miss 率 50% vs 81%:顺序一半命中(数组前半部分被逐出后回流),随机 80% 必须去 DRAM,每次等 100~300 cycles。
通用事件 vs PMU 精确事件不能混用
cache-references/cache-misses与L1-dcache-*/LLC-*是两组不同来源的计数器,计数口径不同,不能直接交叉相除。
4.5 采样热点(perf record)
bash
perf record -e cache-misses -g ./cache_miss rand
perf report --stdio实测热点(随机模式)
bash
# Children Self Command Symbol
94.59% 94.59% cache_miss [.] bench_random
60.00% -0x01 cache_miss [unknown] # 匿名页 / 数据段
37.36% 0.00% cache_miss [unknown] # 内核态 (page walk)
4.22% 2.50% cache_miss [_] std::uniform_int_distribution<…>::operator()(…)
1.05% 1.05% libc-2.17.so [_] __memset_sse2
...解读
bench_random吃掉了 94.59% 的 cache miss——单函数 = 单行代码a[indices[i]]++。[unknown] 60.00%是 RMW 路径上的数据 cache miss,37.36%是内核态 page table walk。__memset_sse2 1.05%是vector(N,0)清零 64 MB 的固定成本。uniform_int_distribution 4.22%是索引生成的开销,与访存主体相比可忽略。
如何定位到"差一行代码"
bash
perf annotate bench_random # 反汇编,看每条指令的 cache-miss 占比
perf record -e mem_load_retired.l3_miss:pp ... # Intel 精确 L3 miss 采样94% miss 收敛到 bench_random → perf annotate 下钻到具体指令 → 精确到一行 a[indices[i]]++。
5. 结论
5.1 实测 vs 预期
| 维度 | 预期 | 实测 | 吻合? |
|---|---|---|---|
| 顺序 vs 随机吞吐 | 5~15× | 11.7× | ✓ |
| stride 阶梯:<16 → 16 → ≥64 | 高 → 断崖 → 底 | 72%/21% → 11% → 7~8% | ✓ |
| 随机 LLC-loads 倍数 | 数百倍 | 855× | ✓(远超预期) |
| 随机 IPC | <1 | 0.66 | ✓ |
| stride=16 断崖 | 预期 ~10~20% | 10.8% | ✓ |
| stride=2~8 仍在高位 | 预期 40~80% | 72%/21% | △ stride=8 低于预期(预取器 stride pattern 未生效) |
| 顺序 L1 miss 率 | 预期 <2% | 17.35% | ✗(冷启动 + 分母被预取压缩) |
两个偏差都已解释:stride=8 预取器未识别 stride pattern(见 §4.2 吞吐趋势分析);顺序 L1 miss 率虚高是分母被预取压缩 + 冷启动(见 §4.4 数据解读第 2 条)。
5.2 一句话总结
实测顺序 vs 随机吞吐差 11.7×、IPC 差 2.8×、LLC-loads 差 855×——差距不在计算量(指令数只差 16×),而在 cycles 总量(差 44×);真凶是 LLC miss 后的 DRAM 等待(随机模式 80% LLC miss ≈ 200 cycle/次)。同一份代码换个遍历顺序就能差出一个数量级,这就是"缓存友好"的硬指标。
6. 关联文档
- tools/code/perf.md —— perf 工具主文档,§六.1.4 缓存层级事件
- concepts/tools/perf-demos-architecture.md —— perf 12 场景学习架构总纲
- concepts/cpu/pmu.md —— PMU 硬件架构与缓存事件底层原理
7. 与 TLB 抖动实验的设计差异
两个实验都是"固定计算量、只改变访存方式、观测硬件瓶颈",但卡在不同的硬件层。详见 demos/tlb-thrashing。
一句话区分
| 本实验(缓存未命中) | TLB 抖动实验 | |
|---|---|---|
| 测什么 | 数据在哪一级缓存(L1 / L2 / LLC / DRAM) | 虚拟地址→物理地址的翻译有多快(TLB / STLB / Page Walk) |
| 瓶颈硬件 | L1D → L2 → LLC → 内存控制器 | L1 dTLB → STLB → PMH → 页表遍历 |
设计差异
| 维度 | 缓存未命中 | TLB 抖动 |
|---|---|---|
| 数组大小 | 固定 64 MB(常量) | 32 KB → 32 MB(自变量) |
| 为什么这样选 | 64 MB > LLC ~30 MB,保证数据永远装不下,缓存行为必然暴露 | 大小本身就是 X 轴——不同大小触及不同数量的 4KB 页,从而测不同 TLB 层级的容量边界 |
| 访问模式 | 顺序 / 随机 / 跨步 (stride=2,8,16,64,256,1024) | 统一跨步 (D=1,2,4,8,...,1024),步长是唯一变量 |
| 步长的物理含义 | stride 越大 → 同一条 cache line 复用越少 | D 越大 → 触及的 4KB 页越多(页数 = 8D) |
| D=16 代表什么 | 跨 64B = 刚好 1 条 cache line,无 L1 复用,开始依赖预取器 | 128 页 (512KB),超过 L1 dTLB (64条) 但远在 STLB (1536条) 之内,性能几乎无变化 |
| D=256 代表什么 | 跨 1KB = 16 条 cache line,预取器也救不了 | 2048 页 (8MB) > STLB 6MB → "断崖式"性能下跌 |
核心差异:渐进式退化 vs 硬容量边界
缓存实验的吞吐随 stride 增大是连续的梯度下降(stride=2 微降 → stride=8 明显降 → stride=64 大幅降 → 随机最低),没有一刀切的阈值。原因是各级缓存大小不同(32KB / 256KB / 13.75MB),每级都构成一个软边界。
TLB 实验存在一个硬容量边界:L2 STLB = 1536 条 × 4KB = 6MB。当工作集 < 6MB 时 page walk 代价很低(~36 cycles),一旦跨过 6MB,PTE 页表被数据挤出 cache,walk 单价从 ~36 拍暴涨到 ~277 拍(7.7×),形成断崖。
互补关系
bash
一次完整的内存访问 = 地址翻译 + 数据读取
地址翻译(TLB 实验的领域) 数据读取(缓存实验的领域)
┌──────────────────────┐ ┌──────────────────────┐
│ VA → L1 dTLB → STLB │ │ PA → L1D → L2 → LLC │
│ → Page Walk → PA │ │ → DRAM │
└──────────────────────┘ └──────────────────────┘
↓ ↓
卡在翻译层:TLB miss 卡在数据层:cache miss
最坏:page walk 走 DRAM 最坏:LLC miss 走 DRAM两个实验分别隔离了访存路径上的两个串行阶段,叠加起来就是一次完整访存的最坏情况:STLB miss + page walk 打内存 + LLC miss + 等 DRAM 返回数据。