Appearance
CPU 硬件视角:流水线、乱序、依赖链、ILP、超线程
面试问“为什么这段汇编/循环慢”常到这一层。不需要懂微架构全家谱,把这几条现象与对策说清即够。
一、现代 CPU 执行模型(就背这三个)
- 流水线化:取指→译码→发射→执行→回写,多级并行。
- 乱序执行(OoO):只要依赖允许,可重排顺序利用空闲执行单元。
- 推测执行 + 分支预测:按预测跳过等待分支结果,猜错就回滚(损耗)。
程序员视角的推论:
- 指令乱序重排是常态 —— 这就是 C++ 内存序/屏障存在的理由,CPU 会重排内存访问。
- 长依赖链(如循环累加
s += a[i])成为延迟瓶颈——即使指令吞吐很高,一条接一条等前一条。 - 分支预测失败代价高(清空流水线 ~10-20 cycles);应让分支可预测(尽量连续真/假成块、少用不可预测的 if)。
二、ILP(指令级并行)与依赖链对策
同一条链上指令无法并行。给性能循环加“多路累加/offload”打破链:
cpp
// 瓶颈:单累加器串行依赖
double s0=0,s1=0,s2=0,s3=0;
for (i; i+4<=n; i+=4){
s0+=a[i]; s1+=a[i+1]; s2+=a[i+2]; s3+=a[i+3]; // 4 条独立链 → 可乱序并行
}
double s=(s0+s1)+(s2+s3);
for(;i<n;i++) s+=a[i];同理 SIMD 点积、FMA 也拆多累加器。这是“为什么展开循环其实有收益”的真相(不是指令少,是拆链 + 少分支)。
三、超线程 / 硬件线程(SMT)
- 一个物理核排两条硬件线程,共享执行单元但各自有寄存器/取指状态。
- 好处:一条线程访存等待时另一条线程用被占的执行单元 → 隐藏延迟(把 cache miss 空窗填上)。
- 坏处:两条都在算时共享执行单元 → 单线程缩放 <2×,甚至可能因争 cache 反而掉。
- 启发:计算密集通常每物理核放 1 线程;混合访存/IO 可放 2;最终以“吞吐/功耗”实测为准。
四、依赖链 vs 吞吐的两种“受限”语言
- 吞吐受限(吞吐 = 每秒能发多少条指令):能做很多独立工作,瓶颈是执行单元/issue 宽度。
- 延迟受限(latency chain):耗在等依赖(进位链、累加器、某些除法/sqrt/fma 延迟高)。 对策可背:延迟受限 → 拆多路/换算法减少链长;吞吐受限 → 向量化/少冗余。
五、访存延迟 vs 吞吐与“算得快但饿着等数据”
点积/GEMM:每取浮点有一串延迟要掩盖。技法:预取 prefetch(__builtin_prefetch/#pragma unroll)、多路累加、blocking 让块在 L1/L2 常驻、非临时存储直写。
六、分支预测题
cpp
if (data[i] > 128) sum += data[i]; // 若顺序排列则可预测快;完全随机则错猜高 → 慢- 可用“分支无依赖转换”:累加器选择性加法(
sum += cond ? v : 0用 predication/select)或先排序再条件聚合工具;但别过度,先证明是热点。 - 代码编profile / PGO 帮 branch layout。
七、常见型号无关经验(别背死)
- 现代服务器核通常有 4 整数 ALU、~2 个 256/512 向量 FMA、分支开销、L1 吞吐高。给 range 不追求精准。
- 问“怎么知道是不是 CPU 频率下降/关了 turbo”:
lscpu、turbostat、lscpu | grep MHz、perf frequency 事件。 - profile 工具会在低水位给 “simd / branch mispredict / llc miss / mem latency / frequency” 聚合——把这些词讲顺即显示功底。
八、一分钟口述模板
「先把热点剖开看是延迟受限(长依赖链)还是吞吐受限(冗余指令)、还是访存/cache 或带宽(数据量大)。延迟链拆多路,吞吐做向量化与去冗余,访存做 tiling + prefetch/对齐;分支用 profile 判断,避免不可预测分支;线程数按物理核 vs SMT 吞吐实际测。」