CPU 架构与性能影响因素:缓存、超线程、NUMA

我在做性能调优的头两年,有个很大的困惑:明明代码逻辑一样,换台机器就跑慢了。后来才明白,CPU 架构的差异才是真正的“幕后黑手”。这一章咱们就聊聊 CPU 里的几个关键设计——缓存、超线程、NUMA——它们怎么影响性能,以及我们在监控时该怎么看待这些指标。

1. 缓存层级:离 CPU 越近,速度越贵

现代 CPU 都用了多级缓存。说白了,就是给 CPU 配了几个不同速度的“小仓库”。

  • L1 缓存:分指令缓存和数据缓存,一般 32KB~64KB,延迟 3~4 个周期。
  • L2 缓存:每个核心私有,256KB~1MB,延迟 10~20 个周期。
  • L3 缓存:多核心共享,几 MB 到几十 MB,延迟 30~60 个周期。
  • 内存:主存,几十 GB,延迟 100~300 个周期。
我的经验:当年调一个高并发消息队列,发现 CPU 的 L2 缓存命中率只有 60%。把热数据按 64 字节 cache line 对齐后,命中率升到 92%,吞吐直接翻倍。你想想看,有时候不是代码逻辑不行,是数据没放对地方。

缓存命中率对性能的影响非常大。用 perf stat -e cache-misses 就能看到真实的 miss 数量。我一般这样快速判断:

# 查看 L1 数据缓存 miss 率
perf stat -e L1-dcache-load-misses,L1-dcache-loads -a -- sleep 5

# 查看 LLC(最后一级缓存) miss 率
perf stat -e LLC-load-misses,LLC-loads -a -- sleep 5
层级 典型大小 延迟(周期) 共享范围
L132~64 KB3~4单核心
L2256 KB~1 MB10~20单核心
L38~32 MB30~60所有核心(或某簇)
主存16~512 GB100~300全系统
关键指标:监控时重点看 cache-misses 和 LLC-load-misses,如果 miss 率超过 5%,大概率是数据局部性不好,需要优化。

2. 超线程:虚拟核心,资源竞争

超线程(Hyper-Threading,HT)把一个物理核心模拟成两个逻辑核心。好处是能多塞一道指令流,提高执行单元利用率。但坏处也很明显——两个线程共享 L1/L2 缓存和执行资源,一旦互相打架,性能反而下降。

我记得有一次做数据库压测,开启 HT 后,TPS 只涨了 10%,但 CPU 占用率却高了 30%。查了 htop 发现,某些逻辑核心在抢同一份 L1 缓存。最后我关掉 HT,单核跑满反而更稳定。

避坑指南:我曾经在虚拟机环境下没关 HT,结果 NUMA 节点内的两个逻辑核心互相干扰,导致数据库查询延迟抖动超过 200%。如果你的应用是计算密集型,建议先测一下关掉 HT 的效果。

在 Linux 里查看 HT 状态:

# 查看物理核心数与逻辑核心数
lscpu | grep -E 'Thread|Core|Socket'

# 如果 Thread(s) per core 大于 1,说明 HT 已开启
# 临时关闭所有 HT(谨慎操作,生产环境别乱试)
echo off > /sys/devices/system/cpu/smt/control

3. NUMA:非一致性内存访问

NUMA(Non-Uniform Memory Access)在多路服务器上很常见。每个 CPU 有自己的本地内存,访问本地的快,访问远端(另一个 CPU 的内存)要过 QPI/UPI 总线,延迟高不少。我见过太多人部署 Redis 时,数据全放在 node 0 上,但进程被调度到 node 1,结果内存访问延迟暴增。

  • 本地内存访问:延迟正常,带宽高。
  • 远端内存访问:延迟高 20%~50%,带宽低。
  • 跨 NUMA 节点通信:还会占用总线,影响整体吞吐。

查看 NUMA 拓扑:

# 显示 NUMA 节点信息
numactl --hardware

# 查看当前进程的内存分配在哪
numastat -p <pid>
调优习惯:我常用 numactl –cpunodebind=0 –membind=0 绑定进程到固定 CPU 和内存节点。对于 PostgreSQL 这类服务,每个实例绑定一个 NUMA 节点,能避免跨节点抢内存。

NUMA 对性能的影响可以用 perf stat -e remote_access 来监控。如果 remote/local 比例超过 0.1,说明内存分配策略有问题。

4. 缓存一致性协议:MESI 和它的开销

多核心共享缓存时,必须保证数据一致性。MESI 协议(Modified, Exclusive, Shared, Invalid)就是干这个的。但它有个代价——当两个核心频繁写同一个 cache line 时,会触发“缓存乒乓”,性能断崖式下降。

我遇到过的一个典型场景:两个线程各操作一个数组,但数组恰好挨着(同一 cache line),结果每次写都要通知另一个核心 invalidation,吞吐直接降到单核水平。解决办法很简单:在变量间加 padding,让它们落在不同 cache line。

实战技巧:用 perf c2c 工具检测缓存行冲突。输出会告诉你哪些 cache line 上发生了“真实分享”(true sharing)和“伪分享”(false sharing)。

5. 综合知识结构

下面这张图概括了 CPU 架构中影响性能的关键因素及它们之间的关系。

CPU 架构性能影响因素概览 CPU 核心 L1/L2/L3 缓存 超线程(HT) NUMA 本地/远端 缓存命中率 / Miss 缓存一致性(MESI) 调优关注:cache-misses、LLC-load-misses、 remote/local 比例、超线程资源争抢

6. 实战中的监控思路

我在做性能瓶颈排查时,一般按这个顺序:

  1. 先用 top/htop 看 CPU 总体使用率,确认不是 CPU 过载。
  2. 然后用 perf stat 看 cache-misses 和 instruction per cycle(IPC)。IPC 太低说明 CPU 经常停等缓存。
  3. 再用 numactl –hardware 检查 NUMA 拓扑,结合 numastat 看内存分布。
  4. 如果怀疑超线程冲突,用 perf c2c record 采集,最后看输出报告。
注意:不要在压测中途改 NUMA 绑定或 HT 状态,会导致结果不可比。我习惯先跑一次 baseline,调完之后再跑一次,对比才有说服力。

好了,这章的内容就这些。缓存、超线程、NUMA 这三样东西,你弄明白了,调优就有方向了。记住一个原则:让数据离 CPU 最近,让线程待在本地。后面我们会具体讲怎么用工具抓到这些指标。