本页目录

性能 I · 测量、Roofline 与向量化

对标:MIT 6.172(性能工程)/ Agner Fog 优化手册 | 前置:csapp-01/02(机器表示、缓存) 性能工程是把"能跑"变成"跑得快"的学问,也是你(数学 + 系统底子)最能出成果的方向之一。它的第一原则反直觉却至关重要:先测量,别猜。这一页建立性能的科学方法——怎么找瓶颈、Roofline 模型怎么告诉你"该优化什么"、以及现代 CPU 的两大加速武器(流水线与 SIMD 向量化)怎么用。

学习层:应该把优化预算花在算术还是搬数据?

具体谜题:一个点积到底能跑多快?

某 CPU 的峰值算力是 \(64\ \mathrm{GFLOP/s}\),可持续内存带宽是 \(16\ \mathrm{GB/s}\)。一个点积每个元素做 2 次浮点运算并读写 12 bytes。若循环的算术强度只有 \(2/12\ \mathrm{FLOP/Byte}\),把乘加指令再优化 4 倍会改变屋顶吗?先算出带宽屋顶,再决定要看 SIMD 还是数据布局。

先预测,再打开实验

写下三条可检验的预测:① 点积的带宽屋顶为 \(16\times2/12\approx2.67\ \mathrm{GFLOP/s}\),因此它先是 memory-bound;② 把只占 5% 时间的函数加速到无穷,总程序最多只快 \(1/0.95\approx1.05\times\);③ 当算术强度超过 \(I^\*=P_{\max}/B=4\ \mathrm{FLOP/Byte}\) 后,Roofline 才换成算力屋顶。

最小心智模型:两个屋顶和一个时间账

把程序看成“搬数据 + 做计算”的组合:Roofline 给出由硬件带宽与峰值算力共同形成的上界,Amdahl 给出局部优化对端到端时间的上界。性能工程不是寻找最漂亮的内核,而是先找当前点受哪一项约束,再测量优化是否把点推向另一面。

形式机制与不变量

设算术强度为 \(I=W/Q\)(FLOP/Byte),带宽为 \(B\),峰值算力为 \(P_{\max}\),则可达性能满足:

\(P\le \min(P_{\max},\,B I),\qquad I^\*=P_{\max}/B.\)

若总时间比例为 \(p\) 的部分加速 \(s\) 倍,端到端加速为 \(S=1/((1-p)+p/s)\)。实验的核心不变量是:同一输入、编译选项、计时窗口和正确性阈值固定,before/after 的差异才可以归因于优化。

反例与失效边界

  • Roofline 是上界模型,不会告诉你分支、同步、NUMA、缓存容量或实现效率;落在屋顶下不等于存在一个简单的优化。
  • 峰值带宽和峰值 FLOP 若来自宣传规格而非同一 workload 的实测,结论只能是数量级判断。
  • 把计时器、I/O 或首次分配混入热循环,会把测量对象改成 harness,而不是算法本身;SIMD 也不能修复错误的复杂度。

迁移任务:写一张瓶颈诊断卡

给你的图像卷积、点积或 N 体代码记录 \(W,Q,p\)、实测带宽/算力和正确性误差,画出优化前后 Roofline 位置。先用 L01 的分块、L07 的 SIMD 和 perf 工具各提出一个假设,再说明哪条硬件计数器会证伪它;不要把“GPU 更快”当作瓶颈诊断。

无 JavaScript 时的静态读法:在 \(B=16\ \mathrm{GB/s}\)、\(P_{\max}=64\ \mathrm{GFLOP/s}\) 时,转折强度是 \(4\ \mathrm{FLOP/Byte}\)。点积 \(I=2/12\) 的 Roofline 上限为 \(2.67\ \mathrm{GFLOP/s}\),属于带宽受限;若只优化一个占 60% 时间的函数到 5 倍,Amdahl 加速为 \(1/(0.4+0.6/5)=1.92\times\)。交互版先让你预测 bound 与加速,再调整 \(p,s,I\) 查看两条上界和诊断账本。

工作负载 算术强度 Roofline 上限 首先检查
点积 0.167 2.67 GFLOP/s 连续访问、L01 分块
高复用矩阵块 8 64 GFLOP/s L07 SIMD、指令吞吐

1. 性能工程的第一铁律:测量,不要猜

Amdahl 定律曲线:不同串行占比下加速比随核数饱和。

图 perf-01.3Amdahl 定律曲线:不同串行占比下加速比随核数饱和。

程序员对"哪里慢"的直觉几乎总是错的——瓶颈常在意想不到的地方(一个没注意的 O(n²)、一次意外的磁盘 I/O、缓存不命中)。所以铁律是:

先剖析(profile)定位真正的热点,只优化那 5% 真正耗时的代码。

方法论:建立"测量 → 找瓶颈 → 优化 → 再测量验证"的闭环,永远用数据说话。每次优化都要有 before/after 的数字,否则你不知道是真快了还是心理作用(甚至变慢了)。

2. Roofline 模型:该优化什么

Roofline 模型:横轴算术强度、纵轴性能,带宽斜顶 + 算力平顶,标内存受限/算力受限区。

图 perf-01.2Roofline 模型:横轴算术强度、纵轴性能,带宽斜顶 + 算力平顶,标内存受限/算力受限区。

优化前要判断:程序是算力受限(compute-bound)还是内存受限(memory-bound)?——这决定优化方向。Roofline 模型用一张图回答:

你的程序落在哪决定策略:

这就是为什么矩阵乘法要分块(csapp-02 的 L01):朴素版内存受限(算术强度低),分块提高数据复用 = 提高算术强度 = 把程序从内存屋顶推向算力屋顶。Roofline 是"先诊断再开药"的性能地图——HPC 工程师的第一张图。

3. 现代 CPU 的两大加速武器

SIMD:一条指令同时算 8 个 float(标量 vs 向量)。

图 perf-01.1SIMD:一条指令同时算 8 个 float(标量 vs 向量)。

朴素的"一条指令一条指令顺序执行"远没榨干现代 CPU。两个必须理解的机制:

① 流水线与乱序执行:CPU 把每条指令拆成多个阶段(取指/译码/执行/写回)流水线并行,还会乱序执行(后面无依赖的指令先跑)、推测执行(猜分支方向提前跑)。

② SIMD 向量化:一条指令同时对多个数据做同样运算(Single Instruction Multiple Data)——AVX 一次算 8 个 float、512 位一次 16 个。理论上直接 8~16× 加速。三种用法:

这就是 [实验 L07]:点积的标量版 / 编译器自动向量化版 / 手写 SIMD 版三者对比,实测加速比——亲手看到"一条指令算 8 个数"的威力,也看到编译器什么时候会/不会帮你向量化。

4. 数据导向设计(DOD):布局即性能

现代性能优化的一大范式转变——从"面向对象"到"面向数据":

5. 练习与要点

例 1(Amdahl 算账) 一个程序 60% 时间在函数 A、40% 在 B。把 A 加速 5×,总加速多少?(\(\frac{1}{0.4+0.6/5}\approx 1.9×\))——理解"为什么优化前必须先知道占比"。

例 2(分支预测实验) 遍历数组累加"大于阈值"的元素,对比数组排序前后的耗时——排序后快数倍,亲眼见分支预测的代价。这是最震撼的性能 demo 之一。

例 3(Roofline 定位) 判断"向量点积"和"N 体引力模拟"各是内存受限还是算力受限(点积算术强度低=内存受限、N 体每对都算=算力受限)——据此决定优化方向,Roofline 思维上手。\(\blacksquare\)

▶ 实验 L07(SIMD 与向量化):labs/L07-simd/ —— 点积三版本(标量/自动向量化/intrinsics)+ 分支预测 demo。跑在 Mac(C)。


下一页:性能 II——缓存优化实战:把 csapp-02 的缓存理论变成具体的优化手法和一个"优化 100×"的大挑战。