2. 理解CPU架构:核心、缓存、流水线、超线程等
做性能调优,不了解CPU怎么工作,等于闭着眼睛开车。你写一行C代码,CPU内部要经历十几步甚至几十步。今天我们就拆开这个"黑盒",看看核心、缓存、流水线、超线程这些部件到底在干嘛。
CPU核心:真的不是越多越好
核心,就是物理上独立的计算单元。每个核心有自己的寄存器、ALU、FPU。你打开任务管理器看到的"物理核心"就是它。
但我见过有人盲目堆核心数,以为16核一定比8核快一倍。结果跑一个串行程序,16核只有1个在工作,其他都在睡觉。你想想看,调度开销反而更大了。优化前先问自己:我的任务能不能并行?
缓存层级:距离决定速度
CPU访问内存太慢了,所以设计了多级缓存。最贴近核心的是L1,最快但也最小;接着是L2,然后是所有核心共享的L3。再往外就是内存,差了两个数量级。
| 层级 | 典型大小 | 延迟 | 带宽 | 位置 |
|---|---|---|---|---|
| L1 Cache | 32KB~64KB(每核) | ~1ns | ~500GB/s | 核心内部 |
| L2 Cache | 256KB~1MB(每核) | ~4ns | ~200GB/s | 核间私有 |
| L3 Cache | 8MB~64MB(共享) | ~15ns | ~100GB/s | 芯片内共享 |
| 主存(DDR5) | 16GB~128GB | ~80ns | ~50GB/s | 片外 |
我曾经优化一个数据库哈希表,每次查询都要跳内存,时延高得离谱。后来把热点数据align到缓存行大小,并且做了prefetch,查询性能直接翻了3倍。这就是缓存友好的力量。
流水线:让指令“流水”起来
一条指令要经过取指、译码、执行、访存、写回多个阶段。如果按部就班做完一条再做下一条,吞吐量太低了。流水线把不同阶段重叠起来:核心在执行当前指令时,同时译码下一条,再取再下一条。理想情况下,每个时钟周期都能完成一条指令。
但流水线怕分支预测失败。如果预测错了,已经预取的指令全得扔掉,重新填充流水线——这就是所谓的“流水线冲刷”。
我在调优一个网络转发程序时发现,switch-case里一个分支命中率只有80%,预测错误率很高。改成无条件跳转或查表后,吞吐提升了12%。
- 流水线深度:现代CPU有14~20级流水线,深度越深,每个阶段做的活越细,频率能更高;但分支预测错误代价更大。
- 乱序执行:CPU不是死板按顺序执行,它会分析指令依赖,让不依赖的指令提前执行,填满流水线气泡。
- 超标量:每个时钟周期发射多条指令(例如4~8条),需要足够多的执行单元。
超线程(SMT):一核双线程的魔法
超线程就是一个物理核心模拟成两个逻辑核心。它利用执行单元的空闲时隙——当一个逻辑核在等缓存命中或等待资源时,另一个逻辑核可以趁机用执行单元。说白了,就是榨干硬件的每一滴性能。
我测试过,超线程通常能提升20%~30%的吞吐量,但如果两个线程都在争抢同样紧缺的资源(比如L1缓存或浮点单元),可能反而变慢。超线程不是银弹。
| 场景 | 超线程效果 | 原因 |
|---|---|---|
| 内存延迟敏感型(如数据库) | 明显提升 | 一个线程等内存时,另一个运行 |
| CPU密集计算型(如矩阵乘法) | 提升有限甚至下降 | 执行单元全部被占,竞争加剧 |
| 多线程并行任务 | 中等提升 | 资源复用效率中等 |
实战建议:架构感知编码
理解这些不是纸上谈兵。我整理了几条能立刻用上的经验:
- 数据对齐:结构体按缓存行对齐(64字节),避免伪共享。
- 分支优化:把高分文概率分支写在if里,低概率用unlikely宏。
- 循环展开:减少分支,提升指令级并行。
- 选择合适的线程数:物理核心数 + 是否启用超线程。IO密集型可以再多一些。
- 使用profiler工具:perf stat能告诉你缓存缺失率、分支预测错误率。
// 伪共享示例 - 糟糕做法
struct Data {
int a; // 核心0频繁写
int b; // 核心1频繁写
}; // 两个变量挤在同一缓存行
// 修正:加padding
struct Data {
int a;
char padding[60]; // 填充到64字节
int b;
};