11.6 CPU Cache 与 NUMA
一句话
现代 CPU 的瓶颈不是 “算得慢”,是 “等数据”:L1 命中约 1ns,主存约 60–100ns;NUMA 再把主存分成 “本地/远程”,远程访问再贵 1.5–2 倍——所以高性能代码做的是 “让数据待在缓存里、待在本地节点上”。
层级与量级
| 层级 | 延迟(量级) | 容量 | 备注 |
|---|---|---|---|
| L1 | ~1ns(4 周期) | 32–64KB | 分指令/数据 |
| L2 | ~4ns(12 周期) | 256KB–1MB | 私有 |
| L3 | ~10–20ns | 数 MB–数十 MB | 多核共享 |
| 主存 | ~60–100ns | GB–TB | 跨 NUMA 再 ×1.5–2 |
| SSD | ~100μs | TB | 与 CPU 差 3 个数量级 |
三个决定性概念
| 概念 | 含义 | 工程动作 |
|---|---|---|
| 缓存行 64B | CPU 按行加载;改一个字节要独占整行 | 让热数据紧凑排列(数组优于链表) |
| 伪共享 | 两个核改同一行的不同字段,缓存行来回失效 | 填充(padding)或按核分片 |
| NUMA | 内存分节点,跨节点访问更慢 | 绑核 + 本地分配(numactl) |
伪共享的典型现象:多线程计数器各自加 1,吞吐反而比单线程低——它们在同一缓存行上互相打。
观测手段
| 工具 | 看什么 |
|---|---|
perf stat | cache-misses、LLC-load-misses、IPC |
perf c2c | 伪共享与缓存行争用 |
numastat / numactl -H | 跨节点分配比例 |
lscpu | NUMA 节点与缓存拓扑 |
调度与缓存的关系见 11.1 进程、线程与调度;观测手段见 11.7 eBPF 与内核态观测、15.9 性能剖析工具链 (火焰图 / perf / eBPF)。
落到数字:跨节点与 miss 的代价
| 访问 | 延迟量级 | 相对代价 |
|---|---|---|
| L1 命中 | ~1ns | 1× |
| L3 命中 | 10–20ns | 10–20× |
| 本地内存 | 60–100ns | 60–100× |
| 远程 NUMA 内存 | 100–200ns | 100–200× |
| 伪共享往返 | 数十–数百 ns | 与核数相关 |
lscpu | grep -iE "numa|socket|cache" # 拓扑
numastat -p <pid> 2>/dev/null || echo "无 numastat"
perf stat -e cache-misses,LLC-load-misses,instructions ./app
反模式
| 反模式 | 后果 |
|---|---|
| 用链表/指针跳转遍历热数据 | 每次跳转都可能 miss,性能差 10 倍 |
| 多线程共享一个计数器 | 伪共享,扩展性归零 |
| 容器不绑核、不感知 NUMA | 跨节点访问,延迟抖动 |
| 盲目开大页 | 分配停顿;对随机访问才有明显收益 |
三条铁律
🔑 顺序访问 + 紧凑布局是最高性价比的优化。
🔑 共享可写字段要分片;否则缓存行争用比锁还慢。
🔑 NUMA 上 “内存跟着 CPU 走”;跨节点访问是隐形的 2 倍延迟。
自检
Q1 为什么 “数组遍历” 比 “链表遍历” 快一个数量级?
▸ 答案:因为数组是连续内存,CPU 预取器能提前把后续缓存行拉进来(顺序访问命中率高);链表每次跳转都是随机地址,几乎每跳一次 cache miss(约 100ns)。当元素很小、数据量大时,差距能到 10 倍以上。
Q2 什么是伪共享?怎么消除?
▸ 答案:两个核分别修改同一缓存行内的不同变量,每次写都要把该行置为独占,导致对方的缓存行失效,来回同步。消除方式:把热点字段各自填充到独立的 64B 行(padding),或按核/线程分片计数,最后再汇总。
Q3 numactl --interleave=all 适合什么场景?
▸ 答案:适合访问模式随机、无本地性的负载(如大内存数据库的全表扫描),把内存均匀分布到各节点可避免单节点耗尽。反之,对延迟敏感、数据有局部性的服务,应该用 --cpunodebind + --membind 绑定到同一节点。
Q4 L3 命中率和吞吐是什么关系?
▸ 答案:L3 未命中意味着要访问主存(约 100ns),而 CPU 一个周期不到 1ns——一次 miss 相当于几百个周期的空转。因此 “LLC 命中率下降 10%” 常常对应吞吐下降 20% 以上,尤其在高并发、大工作集的场景。
Q5 为什么虚拟化和容器会影响缓存与 NUMA 表现?
▸ 答案:虚拟机的 vCPU 可能被调度到不同物理核、跨 NUMA 分配内存;容器共享宿主缓存,邻居的 “缓存污染” 会直接影响你的命中率。对策是绑核、限制 CPU 配额、并接受 “共享 L3 是邻居噪声的来源” 这一现实。
Q6 为什么 “核数增加” 有时反而让吞吐下降?
▸ 答案:因为共享资源的争用上升:L3 与内存带宽被更多核分食、伪共享与锁竞争加剧、跨 NUMA 访问比例变高。表现为 “加核后单核效率下降、总吞吐走平甚至下降”。判断看 perf stat 的 IPC 与 LLC miss 率是否同步恶化。