面对一坨密密麻麻的火焰图、perf stat 输出的几十个计数器、以及一次次改代码却提速不到 1.2 倍的诡异结果我强烈建议你先停下来别再盲目做微优化而是建立一张属于这台 CPU 的 Roofline 模型。这东西就像一张性能“天花板图”一旦画出来你就会清楚地知道当前程序离硬件极限还有多远到底应该优化计算还是优化访存如果今天只有两小时突破口应该在哪里。这篇文章我把 CPU 版的 Roofline 模型从原理到绘制的完整过程拆开你跟着一步步来最后能自己用 Python 画出一条带真实参数参考线的屋顶线并把手里的程序作为一个个点映射上去。全文面向的是做 HPC 优化、高性能计算的开发者也适合刚入门性能分析、想知道“我的程序为什么跑不满 CPU”的同学。1. 面对一坨性能数字为什么非画屋顶线不可1.1 你需要的不是“调优技巧”而是“天花板意识”很多优化新手拿到一个耗时几十秒的程序第一反应是加编译选项-O3 -marchnative然后测一测没提升就换-funroll-loops再不行就尝试手动向量化。这种做法不是完全错但属于典型的“没有方向的瞎撞”。原因很简单你根本不知道这台机器能给你什么也不知道你的程序到底要的是什么。Roofline 模型的厉害之处是它把一台 CPU 最核心的两个能力也就是“每秒能算多少浮点运算”和“每秒能从内存搬多少字节”用一条简单的折线画在同一张图上。横轴是计算强度Arithmetic IntensityAI单位是 FLOP/Byte意思是每个字节的数据搬运能支撑多少次浮点运算纵轴是可达性能Attainable Performance单位是 GFLOP/s。任何一个程序经过测量后都能在图上得到一个点这个点如果贴着屋顶线说明它已经把硬件发挥到极致如果远在屋顶之下说明瓶颈不是硬件而是你的代码质量、并行度或者访存模式。我见过太多人把时间浪费在“优化向量化”上结果程序本身是典型的带宽型任务比如流式计算y[i] a * x[i] y[i]。这种代码读一遍数组、写一遍数组计算量很小再怎么折腾 AVX512性能也只能被内存带宽锁死。另一种情况是程序明明是计算密集型的但因为每次循环都去重新读一遍已经加载到 cache 的数据导致实际表现严重偏带宽型。这两种情况如果不先画图靠肉眼很难判断。1.2 一张图让“优化目标”从模糊变具体Roofline 模型把优化工作总结成一句话先判断程序的点落在屋顶线的哪个区域再决定优先优化什么。如果点落在陡峭的左侧低计算强度区域说明程序是带宽受限的这时候加缓存分块、数据复用、非 temporal 访存效果立竿见影如果点落在屋顶线的拐点右侧且紧贴屋顶说明是计算受限的那么你要关心的事情是有没有用到 CPU 的 FMA 指令有没有用满 AVX 向量宽度线程调度是否有负载均衡问题这类判断在日常优化中有大量实际意义。比如一次寒冬夜里调一个大规模稀疏迭代求解器我用 Roofline 看出所有内核强度只有 0.3 FLOP/Byte 左右带宽已经接近极限于是果断放弃微调循环直接改稀疏矩阵的存储格式和重排算法最终把带宽效率从 60% 拉高到 85%整体提速 1.8 倍。如果当时闷头去优化数学函数调用可能连 10% 都省不出来。这一节的结论是Roofline 不是某种高性能计算竞赛里的理论模型而是一个平凡但实用的决策工具。它把“性能调优”这种动态、模糊的工程问题映射成了一张静态、直观的二维图。基于这台结论接下来就进入实战咱们把这张图一笔一笔画出来。2. Roofline 的数学骨架三条线、一个强度、一个屋顶2.1 计算强度把“每字节算多少”作为核心指标计算强度是 Roofline 模型的灵魂。它的定义很简单[ I \frac{W}{Q} ]其中 (W) 是程序或内核完成的浮点操作次数通常按 FLOP 计(Q) 是程序与内存系统交换的数据量单位是字节。这里的“内存系统”需要特别注意在经典 Roofline 模型中一般指的是 DRAM 层的数据搬运量也就是数据从主存读入 CPU 或者从 CPU 写回主存的字节数。如果把中间各级 cache 的数据搬运也算进去问题就变成“存储层级模型”属于更细粒度的分析但上手阶段先不用管。举例来说一个saxpy内核for (i 0; i N; i) { y[i] a * x[i] y[i]; }它读取了 x 的 N 个浮点数4N 字节读取 y 的 N 个浮点数4N 字节然后写回 y 的 N 个浮点数4N 字节。总访存量为 (4N 4N 4N 12N) 字节。浮点操作数则是一次乘加操作算 2 次浮点运算一次乘法一次加法总共 (2N) FLOP。所以计算强度[ I \frac{2N}{12N} \frac{1}{6} \approx 0.167 \text{ FLOP/Byte} ]这个数值小于 1非常低意味着每字节数据只能支撑不到 0.2 次浮点运算。你马上就能预感到这程序内存带宽一定是瓶颈。这就是计算强度的直观价值它让你用一两个数字就知道程序“吃带宽”还是“吃算力”。2.2 屋顶线的斜率与屋顶高度从带宽瓶颈到计算瓶颈Roofline 曲线由两根线构成一条是水平线代表这台 CPU 的峰值浮点性能 (P_{max})单位 GFLOP/s另一条是从原点出发斜率为 (b) 的直线代表峰值内存带宽 (b)单位 GB/s。这条倾斜直线的含义是如果程序的计算强度是 (I)那么它能实现的最大性能不会超过[ P_{attainable} \le \min(P_{max}, b \times I) ]因为数据搬运量和计算量必须在一个时间段内完成。假设每秒可以从内存搬运最多 (b) 字节而程序每字节只能带来 (I) 次计算那么理论上每秒最多完成 (b I) 次浮点运算。但是就算这个值再高也不能超过 CPU 本身的计算能力 (P_{max})。把这两条线放在同一坐标系下交点代表临界强度ridge point[ I_{ridge} \frac{P_{max}}{b} ]这个点的含义是当程序的计算强度恰好等于这个值时带宽和算力同时达到饱和处于完美平衡。低于它程序属于带宽受限高于它程序属于计算受限。举个具体数字。假设一台 CPU 单条内存通道的理论带宽是 25 GB/s实际 STREAM 测得 20 GB/sCPU 支持 AVX2 双发射 FMA在 3.5 GHz 下单核的峰值浮点性能约 56 GFLOP/s。那么临界强度 (I_{ridge} 56 / 20 2.8) FLOP/Byte。如果程序强度只有 0.5那么就算把算术单元全用上也白搭因为内存搬不过来这时候优化访存模式、减少数据搬运才是让点向右侧移动的关键。很多教材喜欢画“多层屋顶线”比如分别针对标量、SSE、AVX、AVX512 画不同高度的水平线每条水平线对应不同-march级别的指令集。CPU 版的 Roofline 如果用多层屋顶复杂度和表达能力都有提升但对于首次建立模型的人我建议先画整体单层屋顶后面再按需分层。基础模型永远是从单层开始的。2.3 别忘了“可达性能”和“峰值性能”之间的差理论数学公式摆在这儿但实际硬件有个残酷的现实没有任何实测程序能做到 100% 的峰值带宽或者 100% 的峰值浮点性能。STREAM 测试通常只能跑到理论带宽的 80% 左右计算密集型的高性能代码能跑到峰值 FLOPs 的 60%-90% 已经相当不错峰值只是理论上限Roofline 的屋顶线应当画在“实际可达到的峰值”上否则会给人一种错误的乐观预期。具体到实践我通常的做法是用 STREAM benchmark 实测的内存带宽作为屋顶斜率基准用峰值浮点性能乘以一个 0.8~0.9 的经验系数作为水平屋顶。这样画出的模型更贴近真实“极限”。后面写代码时你会看到这类务实指标让优化决策更有操作性。3. 手把手测出 CPU 的两个天花板参数3.1 峰值 FLOPs不要盲目相信宣传单自己算一遍建立 Roofline 模型的第一件事是测量或推导出这台 CPU 的峰值浮点性能。最靠谱的方法不是去搜索引擎查“某某 CPU FLOPs”因为厂商给的数字通常用满全部核、最高 Turbo 频率和 FMA 指令集说不定还带稀疏算法加速未必能直接套用到你的程序上。更可靠的是自己根据 CPU 规格推算一遍。对于现代 x86 CPU峰值浮点每秒运算次数可以按如下公式估算[ P_{max} \text{核数} \times \text{每核每周期浮点操作数} \times \text{CPU频率} ]其中“每核每周期浮点操作数”取决于指令集。以 Intel/AMD 的 AVX2 为例一个 256 位向量可以容纳 4 个双精度浮点数或者 8 个单精度。FMA 指令在一个周期内完成一次乘加相当于 2 次浮点操作。如果普通标量乘法需要 1 个 FLOP那么 4 颗执行单元并行执行 FMA 时每周期可以完成 (4 \times 2 8) 次双精度浮点操作。不过这是简单基于向量宽度和 FMA 计算的“理想值”不少 CPU 实际还受执行端口数量限制所以更保险的方法是用一个小基准测一下。我常用的是下面这段 C 代码编译时开启-O3 -marchnative利用 OpenMP 并行尽量贴近 FMA 的极限。#include stdio.h #include omp.h void fma_loop(double *a, double *b, double *c, int N) { #pragma omp parallel for for (int i 0; i N; i) { for (int k 0; k 10000; k) { a[i] a[i] * b[i] c[i]; } } } int main() { int N 1 20; double *a (double*)malloc(N * sizeof(double)); double *b (double*)malloc(N * sizeof(double)); double *c (double*)malloc(N * sizeof(double)); // 初始化略 double start omp_get_wtime(); fma_loop(a, b, c, N); double end omp_get_wtime(); double flops (double)N * 10000 * 2 * omp_get_max_threads(); printf(GFLOP/s %f\n, flops / (end - start) / 1e9); }这段代码每个 a[i] 被反复更新数组总大小 24 MB足够把 L3 缓存之外的内存也压一遍。不过注意由于它一个数组被反复访问实际可能被 cache 吸收一部分访存但它的设计目标是压计算指令吞吐所以并不是在测带宽而是在测 FLOPs 的实践上限。实测得到的数值比我基于规格推算的峰值低 10%~20%这很正常因为 RAM 情形依赖具体的迭代指令流。如果你不想写代码还可以直接读 CPU 的规格参数比如象棋跑分、linpack之类但一定要关注测试用的线程数和频率。用lscpu看核心数、看 CPU 主频再用turbo状态校准这些细节都会影响精度。3.2 实测内存带宽STREAM 基准的正确跑法与常见陷阱内存带宽这个值标准做法是用 STREAM benchmark。它的核心是一组简单的内存操作copy: (c[i] a[i])scale: (b[i] s \cdot c[i])add: (c[i] a[i] b[i])triad: (a[i] b[i] s \cdot c[i])运行后它会报告每项操作的平均带宽MB/s。这个数值基本代表了你用普通memcpy风格代码时能达到的“实际最大带宽”。STREAM 的正确跑法有几个关键点数组规模必须显著超过 CPU 的最大 cache 容量。比如一块 CPU 有 32 MB L3那么数组长度建议设置成最大 cache 的 4~6 倍保证数据真的从内存中来。跑 STREAM 时默认数组长度是 2,000,000 个 double16 MB对现代 CPU 通常偏小我一般用-DSTREAM_ARRAY_SIZE80000000让它变成 640 MB确保绝对不依赖 cache。使用与目标应用一致的线程数。STREAM 支持 OpenMP可以用-OMP_NUM_THREADS核心数跑满核也可以单独跑单核版本两者测出的带宽差异非常大对 Roofline 的屋顶斜率影响直接。注意 NUMA 内存分配。多路服务器上默认 STREAM 跑出来的带宽可能因为所有线程只绑定在一个内存节点上而低于理论值。尽量用numactl --interleaveall或者--physcpubind合理绑定线程。结果取多次运行的最大值而不是平均值。因为内存带宽受系统噪声和频率波动影响很大取最大值更能代表硬件天花板。编译命令很简单gcc -O3 -fopenmp -DSTREAM_ARRAY_SIZE80000000 -o stream stream.c OMP_NUM_THREADS16 ./stream观察 STREAM 输出中的Triad值即可。比如双通道 DDR4-3200 的理论带宽约 51.2 GB/s实测 Triad 通常能跑到 45~48 GB/s。一旦拿到这个数值就相当于拿到了 Roofline 模型里最关键的斜率。3.3 用硬件计数器验证而不是靠口头估计如果想把峰值浮点性能也评估得更准可以借助perf stat来统计实际运行的 FLOPs然后用实测运行时间反推。比如对一个已知浮点数量的程序perf stat -e fma_retired,avx_aux,avx_instr,inst_retired ./myapp得到 FMA 指令数后再根据每条 FMA 指令的向量宽度比如 AVX2 是 4 个 double换算成 FLOPs。这个数比“我估摸大概 60 GFLOP/s”可靠得多。不过初学者一开始不必深挖硬件计数器先把模型的原始参数用“自己推导 STREAM”得到等后期定位优化效果时再用 perf 也不迟。4. 画一条属于自己的 Roofline 曲线4.1 用 Python 三步画出屋顶线拿到 P_max 和 b 后画图就非常快了。我用的是 Python Matplotlib不需要任何重型依赖。下面的代码请直接保存为roofline.py使用。import numpy as np import matplotlib.pyplot as plt # 参数根据自己实测结果替换 P_max 280.0 # GFLOP/s多核AVX2 FMA实际峰值 bw 45.0 # GB/sSTREAM Triad实测带宽 I np.logspace(-3, 3, 500) # 计算强度从0.001到1000 attainable np.minimum(P_max, bw * I) plt.figure(figsize(8, 6)) plt.loglog(I, attainable, linewidth2, labelRoofline (CPU)) # 标注临界强度点 I_ridge P_max / bw plt.axvline(I_ridge, colorgray, linestyle--, alpha0.6) plt.text(I_ridge * 1.2, P_max * 0.5, fRidge Point: {I_ridge:.2f} FLOP/Byte, rotation90, vacenter, fontsize9) plt.xlabel(Arithmetic Intensity (FLOP/Byte)) plt.ylabel(Attainable Performance (GFLOP/s)) plt.title(CPU Roofline Model) plt.grid(whichboth, alpha0.3) plt.legend() plt.tight_layout() plt.savefig(roofline_cpu.png, dpi150)这段代码只有几十行但以下几个细节值得注意横纵坐标都采用对数刻度因为计算强度和性能跨度往往有几个数量级线性坐标会把低强度区域压扁看着不明显。对数坐标是 Roofline 模型的标配。两个关键参数用变量放在开头后续如果换了机器只改两个数字就能画出新图。临界强度点用竖虚线标注出来一眼就能知道当前程序属于哪个区域。4.2 多核与 SIMD 对屋顶线的影响CPU 版本的专属变体同一颗 CPU用单线程和用完整 16 线程绘制的屋顶线差别巨大。因为多核下的总内存带宽虽然也会提升但往往不会随核心数线性增加比如两颗 CPU 在单核时内存带宽只有 12 GB/s16 核全开则可以到 80 GB/s而峰值浮点性能会从 12 GFLOP/s 变成 200 GFLOP/s。这意味着不同线程数下的临界强度完全不同所以当你分析多线程程序时一定要使用“与该程序实际线程数相匹配的屋顶线”。同样SIMD 指令集的宽度也会改变屋顶高度。一台支持 AVX512 的 CPU单核浮点峰值比只跑 SSE 高 4 倍但内存带宽不变。于是这类 CPU 的临界强度会右移更多程序会落到带宽受限区。这在实践中是一个非常深刻的洞察每次引入更宽的 SIMD本质上就是把屋顶的水平线抬得更高而很多原本计算受限的程序会因此慢慢变成功带宽受限的程序于是接下来优化的重心就必须从向量化转向数据驻留和带宽效率。为了反映这种差异可以在一张图上画多条屋顶线单核标量、单核 AVX2、多核 AVX2 等。这是 CPU 版 Roofline 的最常见形式。绘图逻辑就是循环不同的 P_max 和 b每次用同一数学模型生成一条线只是参数不同。我实际做过类似的图左侧低速率的屋顶线来自单核 SSE中段是单核 AVX2再往上是多核 AVX2最后全核带 FMA 的水平线最高。应用点只有落在最上面的那条线上才说明你榨干了 CPU 的每一滴性能。5. 把真实程序钉到图上从性能计数器到计算强度5.1 用 perf 统计指令数/事件算出程序的实际强度有了屋顶线下一步是要把你正在调优的程序表示成图上的一个点。这个点的横坐标是程序的计算强度纵坐标是它实际达到的 GFLOP/s。计算强度需要两个数据浮点操作数和访存字节量。前者可以用性能计数器近似。Linux 下最方便的工具是perf stat常见计数器名字如下fma_retired退休的 FMA 指令数量每条按向量宽度换算 FLOPs。fp_arith_inst_retired/fp_arith_simd_inst_retired现代 CPU 上浮点算术相关指令的计数有的还区分标量、128 位、256 位、512 位指令。cache-misses/l3_cache_misses可用于估算内存层级的存取次数。instructions/cycles用于流水线统计。一条典型的测量命令是perf stat -e instructions,cycles,fp_arith_simd_inst_retired.256b,cache-misses ./myapp注意不同架构的事件名可能不同建议先用perf list查看当前 CPU 支持的具体事件名。拿到 FP 指令数后做 FLOPs 换算一条 256 位 AVX 指令处理 4 个双精度或 8 个单精度和 4 个乘、4 个加等价于 8 次浮点操作。一条标量 FMA 指令等价于 2 次浮点操作。一条 512 位 AVX512 指令如果包含 FMA等价于 16 次双精度浮点操作。如果你的循环代码比较规整也可以直接从逻辑上统计 FLOPs。比如矩阵乘法 C[M][N] A[M][K] * B[K][N]每个输出元素需要 K 次乘加所以总 FLOPs 约 (2 \times M \times N \times K)。这种方式简单可靠只要源码是你自己写的不会引入计数器解析的误差。访存字节量的估算同样重要。估算思路是用cache-misses粗略代表从 DRAM 读数据的事件数每次 cache miss 实际搬运一个 cache line通常 64 字节。写数据要看你使用的是普通写最终会回写还是无临时写直接写。普通写也算一个 cache line 的写带宽。一个更粗略但常见的做法是直接用程序的数据结构大小乘以访问次数比如流式程序就按遍历数组的总字节数算。由于 Roofline 的目的不是做极精确的硬件建模而是提供量级判断所以这种估算误差在 2~3 倍以内都可以接受因为它们反映的是“程序本质属性”而不会改变它落在屋顶左侧还是右侧。5.2 读图你的程序是“带宽型”还是“计算型”把横纵坐标都算出来后立马在图上打点。我习惯写一个小脚本批量处理多个内核比如把单核版本、多核版本、不同分块策略分别打上不同颜色一眼就能看出优化措施的效果。读图的逻辑可以拆成以下几步如果点在屋顶右侧且远离屋顶比如纵坐标只有屋顶的 30%说明程序是计算密集型但并没有把计算单元吃满。这时候应该查编译器生成的指令是否包含 FMA、是否用了 AVX、是否因为依赖链导致每周期执行的指令数太低。如果点在屋顶左侧且远离屋顶说明程序访存强度低又没吃到带宽可能存在大量 cache miss、伪共享、或者内存随机访问。优化方向是空间局部性、数据结构紧凑化、分块缓存。如果点贴着屋顶线说明硬件资源已经饱和这时候再优化这条内核的收益会非常有限不如从算法层面减少 FLOPs 或者数据搬运量。举个例子我调过一个稀疏矩阵向量乘SpMV一开始的强度只有 0.2 FLOP/Byte点在屋顶左侧且低于屋顶 40%。于是我做了重新排序和压缩存储把访存连续化实测带宽利用效率提升强度的确没多大变化但点往上走贴近屋顶了耗时降了 1.6 倍。这就是“在带宽受限区优化访存”典型收益。6. 一个实例矩阵乘法与稀疏向量到底差在哪6.1 密集矩阵乘理论上更接近计算密集区但实际需要调优以经典的 (C A B) 为例(MNK1024) 的双精度矩阵乘法。总 FLOPs 为[ 2 \times 1024^3 \approx 2.1 \times 10^9 ]如果数据全部放在内存中一次访存就把所有 A、B、C 三个矩阵的各 8 MB 读一遍总数据量约 24 MB。计算强度为[ I \frac{2.1 \times 10^9}{24 \times 10^6} \approx 87 \text{ FLOP/Byte} ]这个强度远大于大多数 CPU 的临界强度通常 1~10所以矩阵乘法属于天然的计算密集型算法理论优化核心方向一定是提高计算指令的效率而不是压内存带宽。这也是为什么矩阵乘法基本都要搬出BLAS库其中一个关键原因是 BLAS 能通过分块把数据复用到极致实际访存量比朴素三重循环低得多强度变得更高甚至变成 100 FLOP/Byte 量级。实测表现上普通的 (i-j-k) 三重循环写的矩阵乘跑出来的点大概率不在屋顶上而是明显偏下。原因不是数据带宽的问题而是循环结构太差没有复用寄存器也没有用 SIMD计算单元效率极低。此时 Roofline 判断它“计算受限”并不意味着你已经把计算能力用满了而只是说硬件理论上限由计算能力决定你的代码离极限还远。要做的仍旧是优化计算路径、展开、向量化。6.2 稀疏操作强度低得吓人优化内存布局才见效反过来看 SpMV每行为非零元素做一次乘加读取同时读取向量 x。假设每行平均 10 个非零元素双精度值 列号各占 8 字节和 4 字节如果用 32 位索引加上 x 访问和 y 的写回。粗略估算一个非零元素的计算量 2 FLOP而访存量至少 12 字节计算强度[ I \frac{2}{12} \approx 0.17 \text{ FLOP/Byte} ]比 Roofline 的临界强度低十几倍。这就注定 SpMV 无论怎么加 SIMD、怎么展开循环都不可能达到很高的 GFLOP/s因为内存带宽约束了性能上限。优化方向应该是减少非零数据存储开销例如用 CSR 的优化 variant、用 16 位索引、对 x 做 cache blocking、或压缩向量访问的自定义格式这些办法都不能提高强度但可以让程序在带宽受限屋顶下跑得更接近极限。把这两个例子放到同一张 Roofline 图上你会发现矩阵乘法的点远远落在右侧高处SpMV 的点则被“钉”在左侧低处。这种视觉冲击比任何文字解释都更能让你理解“为什么不同算法优化策略完全不一样”。6.3 实际优化验证以点移动方向作为指标我曾经给一个模拟计算项目做过小型实证先用朴素算法跑一遍矩阵乘和 SpMV算出各自的点然后分别做针对性优化。矩阵乘优化使用-O3 -marchnative并改用分片循环点从纵坐标 20 GFLOP/s 升到 180 GFLOP/s逼近屋顶线但横坐标没变多少SpMV 改用压缩的本地索引并融合写点从带宽屋顶下 28% 升到 82%横坐标也从 0.17 提到 0.21。这两条路径的优化工作完全不同但验证标准都是“点的坐标是否向屋顶移动”。如果工作中你发现某个优化耗时多但点没动甚至横纵坐标都没变那基本可以判断这次优化没触及瓶颈赶紧换个方向。7. 建立 Roofline 模型时最容易翻车的四件事7.1 峰值 FLOPs 的估算翻车忽略 FMA、Turbo、核数第一个坑是拿着 CPU 标称主频算峰值但忘记 Turbo Boost 睿频的存在。现代 CPU 全核睿频一般低于单核睿频而且温度墙、功耗墙都会让实际频率浮动。比如一颗标称 3.8 GHz 的 CPU全核满载可能只有 3.2 GHz。如果不注意你画出的屋顶线会虚高 20%用这个作为参照系很容易得出“程序只用了 60% 峰值”的错误结论。第二个坑是忽略 FMA 指令。早期 SSE 时代一个周期做一次乘或一次加算峰值时只按 1 次操作/周期/单元。但现代 CPU 有了 FMA乘加合并成一条指令每周期能完成 2 次浮点操作。如果你只按基础 SSE 宽度的 1 倍算屋顶会明显偏低很多计算受限程序会被错误地归类为“已经接近峰值”。我用英特尔的实测算过忽略 FMA 会让峰值估算低一半这是影响最大的一个失误。第三个坑是核数。多核时峰值浮点性能基本按核数线性扩展但内存带宽扩展不是线性的。所以你要特别注意给模型标注场景这是“单核模型”还是“16 核模型”不同场景画出来的屋顶线完全不是一回事。7.2 带宽测量被缓存欺骗或反被超线程拖垮STREAM 测量如果数组太小比如只有 2 MB被测对象就完全落在 L3 或者 L2 里报出来的“带宽”能跑几百 GB/s这其实是缓存带宽根本不能画 Roofline。正确做法是让数组总量远超最后一级缓存。我习惯使用 512 MB 到 1 GB 的数组进行测试同时在结果中注意每项报告的带宽是否随数组尺寸变化如果变小说明起效了。另一个坑是超线程。超线程会在物理核之间共享执行资源但不同核的并发访问对内存控制器压力很大。如果你用OMP_NUM_THREADS物理核数*2去跑 STREAM可能带宽反而比只用物理核数更低因为逻辑核竞争导致频繁切换所以通常建议绑定物理核而不是全部逻辑处理器。7.3 计算强度里的“字节”到底按什么算——有符号与无符号这是初学者最容易懵的地方。Roofline 的横轴计算强度它的分母 Q 是该从“主存-缓存接口”上统计的字节数而不是程序源码里sizeof(double)*N那么简单。举例来说y[i] a * x[i] y[i]如果 y 先 write-allocate写操作会先读入一个 cache line再修改最后回写。这里实际访存量可能是 (4N)x 读 (4N)y 读因为 write-allocate (4N)y 写回总共 12N而如果你用了 non-temporal store 来避免写分配则可能少掉 4N 的读变成 8N。同样的计算逻辑计算强度会从 0.167 变成 0.25这足以改变你把它判断为“带宽极窄型”还是“中等带宽型”的结论。因此当你看到某个程序计算强度报告是 0.3不要直接跑去改算子顺序先自问我统计访存字节时有没有算上写分配的额外开销不同 CPU 和编译器会有不同策略如果不确定宁可采用偏保守较大访存量的估计这样不会得出过于乐观的结论。7.4 只画一条屋顶线没有画多核/AVX 的“多层屋顶”最后这个坑更多是绘图层面。CPU 版 Roofline 与传统单处理器版本相比一个重要特性就是“屋顶不止一层”。用多线程时水平屋顶升高用 AVX512 时水平屋顶进一步升高但带宽屋顶通常只和内存控制器有关多核时斜率会变陡但并非线性。如果你只画一条单核 SSE 屋顶线然后拿一个全核 AVX512 程序去对照哪怕程序性能再强点也永远在屋顶以上这就闹笑话了。我的建议是建图时用三种配色分别画三层屋顶线单核标量、单核 AVX2、全核 AVX2如果支持 AVX512 再叠加一条。然后你把自己的程序按照线程数和指令集标到对应的屋顶底下。分析时只和“同场景屋顶线”做对比这样才会得到有效结论。其实不同的编译器选项、不同的指令集扩展本质上改变了程序可用的屋顶高度。想要看-mavx2对程序的影响就分别用两种编译方式跑同一程序看它的点是否在同一屋顶下移动。到这里你已经完整掌握了 CPU 版 Roofline 模型的建立流程。最后再分享一个我自己的小习惯我不是等程序写完了才画 Roofline而是每写一个重要计算内核就顺手把它的强度和实测性能点更新到图上长期维护这张图。这样做的好处是项目里所有核心模块的性能天花板一目了然看到新需求时第一反应不是“再去网上搜一段优化技巧”而是“我这张图告诉我这个模块应该属于哪一侧它的优化预算应该是多少”。很多连续调优的项目最终形成的就是一张布满点、注满注释的 Roofline 图它比任何优化报告都诚实、都清晰。
阅读完成 · 觉得有帮助?