flops.c 告诉你 CPU 每秒能做多少道浮点算术题,但一台处理器的能力远不止于此。整数跑得快吗?内存带宽够吗?碰到除法和开方会不会腿软?本文用四个诞生于学术论文的经典基准,把这些问题一个一个回答清楚。
前情提要:flops.c 测了什么、没测什么
在上一篇文章 你的电脑每秒能做多少次浮点运算?—— FLOPS 的故事中我们介绍了 flops.c——一个 1992 年由 Al Aburto 编写的浮点性能测试程序,其中精心设计了 8 个定积分模块,每次循环的加减乘除次数都精确可数,最终算出 MFLOPS。
但 flops.c 有一个可能是故意为之的"盲区":所有数据都在 L1 缓存里,不碰内存。这意味着 flops.c 测的是 CPU 浮点管线的纯吞吐上限,完全不反映内存带宽、整数性能、或者某些"昂贵"指令(除法和开方)的真实代价。
一台完整的处理器,需要从更多维度来审视。除了 flops.c 之外,计算机体系结构领域还有一些非常经典的测试基准,每套背后都有相关研究基础。
以下四个基准,我们全部用 C 和 Python 两版实现。
一、Dhrystone — 不碰浮点的整数性能
Weicker, Reinhold P. "Dhrystone: A Synthetic Systems Programming Benchmark." Communications of the ACM, Vol. 27, No. 10, pp. 1013–1030, October 1984.
名字的梗:"Dhrystone"是对"Whetstone"(另一个浮点基准)的文字游戏——Whet(磨刀石)是湿的,Dhry(干)拼写故意错成"dry",暗示这个基准完全没有浮点运算。
测什么
Dhrystone 测量 CPU 的纯整数性能。它构造了一个"合成程序"——不长,但包含了系统编程中最常见的操作模式:函数调用(带参数和不带参数)、指针间接寻址、字符串拷贝和比较、整数四则运算、分支跳转。每种操作的占比是根据对大量真实程序的统计来确定的。
怎么报告结果
输出两个数:
- Dhrystones/second:每秒执行了多少次主循环
- DMIPS:以 VAX 11/780 为基准标准化——这台 1977 年的小型机跑出了 1757 Dhrystones/s,定义为 1 DMIPS。你现在的电脑大概是 几万 DMIPS 级别
注意事项
不要用 -O3 编译 Dhrystone。Dhrystone 的核心价值在于测试函数调用开销和指针追逐,而现代编译器在 -O3 下会把很多调用内联展开,大量指针操作被直接优化成寄存器移动——这时候测出来的已经不是 CPU 性能了,而是编译器有多聪明。用 -O2 跑,禁用危险的链接时优化。
实测数据(Apple M 系列,单线程)
| 语言 | Dhrystones/s | DMIPS |
|---|---|---|
| C (gcc -O2) | ~26,000,000 | ~14,800 |
| Python 3 | ~10,300 | ~5.9 |
Python 的 Dhrystone 只有 C 的 0.04%——这比 flops 的差距(C 的 1–2%)还要夸张一个数量级。因为 Dhrystone 的核心操作是函数调用和属性访问,恰好是 Python 解释器开销最大的地方——每次 obj.attr 都是一次哈希字典查找。
二、STREAM — 内存带宽的上限在哪里
McCalpin, John D. "Memory Bandwidth and Machine Balance in Current High Performance Computers." IEEE Computer Society TCCA Newsletter, December 1995.
持续更新版:https://www.cs.virginia.edu/stream/
名字的梗:就叫 STREAM,全大写,取"数据流"之意。四位向量运算像四条水流从内存流进流出。
测什么
STREAM 测量的是 CPU 和主存(DRAM)之间的可持续带宽。它做四件极简单的事:
| 内核 | 代码 | 每元素数据量 | 每元素 FLOP |
|---|---|---|---|
| COPY | a[i] = b[i] |
16 字节(读 8 + 写 8) | 0 |
| SCALE | a[i] = q × b[i] |
16 字节 | 1 |
| ADD | a[i] = b[i] + c[i] |
24 字节 | 1 |
| TRIAD | a[i] = b[i] + q × c[i] |
24 字节 | 2 |
就这。没有花哨算法,没有 cache 优化技巧。
核心设计原则:数组必须大到塞不进缓存
STREAM 默认给每个数组分配 ~500 MB(8 字节 × 6400 万 double),三个数组加起来约 1.5 GB。这还勉强够——如果将来 L3 缓存做到几百 MB,就得跟着加大数组。
当你读写的数据远大于任何缓存时,CPU 的预取器和内存控制器就会原形毕露——这才是真正的 DRAM 带宽,不是 L1/L2 的虚假安慰。
为什么用"最佳值"而非平均值
STREAM 的规矩:跑 10 次,取最快的那一次上报。这不是为了吹牛——因为最好的那次排除了所有操作系统中断、上下文切换、TLB miss 等随机干扰,最接近硬件极限。内存带宽的理论值就是"最短时间能传完所有数据",最佳值才匹配这个定义。
实测数据(单通道 DDR5,单线程)
| 内核 | 带宽 (MB/s) |
|---|---|
| COPY | ~64,000 |
| SCALE | ~60,000 |
| ADD | ~61,000 |
| TRIAD | ~61,000 |
这些数字和三通道/四通道内存、NUMA 架构、以及 -fopenmp 多线程编译选项息息相关。单线程模式下你只能看到单个内存控制器的吞吐上限。
三、FFT Benchmark — 复数算术
Cooley, James W. and Tukey, John W. "An Algorithm for the Machine Calculation of Complex Fourier Series." Mathematics of Computation, Vol. 19, No. 90, pp. 297–301, April 1965.
历史地位:这篇论文把 DFT 的计算复杂度从 O(N²) 降到了 O(N log₂ N),直接催生了数字信号处理这个行业。没有 FFT,就没有 MP3、JPEG、4G/5G、MRI 成像。(请同学们搜索一下DFT和FFT各自是什么的缩写。)
测什么
FFT 测试的是三个维度的组合:
- 复数乘加:每个蝴蝶(butterfly)操作包含 4 次实数乘法 + 6 次实数加法。如果 CPU 有 FMA(融合乘加)指令,编译器可以在这里大显身手
- 跨步访存:FFT 的每一层分解,数组访问间距以 2 的幂次变化——从相邻到跨越半个数组。这折磨着缓存预取器
- 三角函数评估:每个蝶形需要
exp(-2πi/N)的 twiddle factor。我们这版实现预计算了复指数(用标准库的cexp),不算在 FLOP 统计内,但它的执行时间仍然被包含了
精确 FLOP 计数
和 flops.c 一样,FFT 的 FLOP 也是精确可数的:
总 FLOP = 5N × log₂N
每个蝶形((u+v) 和 (u−v) = 4 实数加,w×v = 4 实数乘 + 2 实数加)= 10 实数运算。每层有 N/2 个蝶形 = 5N FLOP,总共 log₂N 层。没有任何近似。
实测数据(N=65536, 单线程)
| 语言 | MFLOPS |
|---|---|
| C (gcc -O2) | ~2,400 |
| Python (纯) | ~35 |
差距约 70 倍——Py 的 FFT 比 flops 表现好(因为 FFT 的计算密度高,分摊了 Python 循环开销)。加上 NumPy(底层是高度优化的 C + 可能的 FFTW 调用),性能瞬间追到 C 的十几倍——因为 NumPy 的 FFT 用了 SIMD 和手工优化的蝶形调度,远超朴素实现。
多尺度报告
FFT 基准额外输出了从 N=256 到 N=65536 的多尺度 MFLOPS 表。有意思的观察:小 N(256)时 L1/L2 缓存完美覆盖,MFLOPS 最高;大 N 时数据溢出 L2 进入 L3,性能会跌一个台阶;再大到溢出 L3 时再跌一次。这张表本身就是一张非正式的缓存层级探测图。
四、N-Body — 当 CPU 遇到除法和开方
Barnes, Josh and Hut, Piet. "A Hierarchical O(N log N) Force-Calculation Algorithm." Nature, Vol. 324, pp. 446–449, December 1986.
(直接 O(N²) 法更古老:von Hoerner, S. Zeitschrift für Astrophysik, 50:184, 1960)
物理背景:N-body 模拟是天体物理的标准工具——计算 N 个天体在相互引力作用下的运动轨迹。每个天体受到其他 N−1 个天体的引力,每对天体之间需要算一次 1/sqrt(r²)。
测什么
这个基准测的是 CPU 的除法和开方吞吐量。
在 flops.c 的 8 个模块里,只有模块 7 有 25% 的除法——而且那些除法都藏在多项式里,编译器可能优化掉一部分。N-body 不一样:每次内层循环必定执行 1.0 / sqrt(r²),既不能去掉除法,也不能近似开方。这两个操作在现代 CPU 上通常是浮点管线里延迟最高的一对(10–30 个周期 vs 加减法的 1–2 个周期)。
核心内循环
dx = bj.x - bi.x;
dy = bj.y - bi.y;
dz = bj.z - bi.z;
r2 = dx*dx + dy*dy + dz*dz + EPS2; // EPS2 防止除以零
inv_r = 1.0 / sqrt(r2); // ← 瓶颈在这里
inv_r3 = inv_r * inv_r * inv_r;
bi.ax += mj * dx * inv_r3 * G; // ×3 分量
每次配对约 27 FLOP,但时间大头花在那一条 1.0/sqrt 上。
N 的选择很重要
- N = 100–500 :全部数据在 L1 缓存内,纯测 CPU 的除法/开方吞吐 —— 这才是这个基准的设计意图
- N = 10000+ :数据溢出缓存,变成内存带宽测试(就像 STREAM 了),失去了本意
所以我们默认 N=200,输出 200、300、500 等多档,方便观察缓存边界。
能量守恒校验
这个基准有一个天然的正确性检查:能量守恒。引力场是保守场,系统的总能量(动能 + 势能)在时间演化中应该保持不变。我们跑完模拟后计算初始能量和最终能量,报告能量漂移百分比。如果漂移超过 1e-6,说明要么时间步长太大,要么实现有 bug。实测漂移通常在 1e-10 级别——机器精度以内。
实测数据(N=200, 单线程)
| 语言 | MFLOPS |
|---|---|
| C (gcc -O2) | ~3,500 |
| Python (纯) | ~55 |
差距约 60 倍。这个差距比 FFT 小一点——因为纯 Python 的 sqrt 和 / 直接调的是 C 标准库,绕过了 Python 的大部分解释开销,省下来的时间把比例拉近了一些。
这四个基准和 flops.c 是什么关系
| 维度 | flops.c | 本套基准 |
|---|---|---|
| FP 加减乘 | ✅ 核心测试 | FFT(复数)、N-body(带除法和开方) |
| FP 除法和开方 | 部分(模块 7 占 25%) | N-body — 每次迭代必定执行 |
| 整数性能 | ❌ 完全没有 | Dhrystone — 100% 整数 |
| 内存带宽 | ❌ 故意规避(全在 L1) | STREAM — 故意超出缓存 |
| 函数调用开销 | ❌ 一个 main 函数 | Dhrystone — 核心测试目标 |
| 并行 | pthreads / goroutines / mp | STREAM 支持 OpenMP |
| 文献 | 1992 技术报告 | 1965–1995 五篇学术论文 |
把各种方法组合起来,你得到是一台处理器相对完整的能力图谱。
怎么跑
git clone https://github.com/Kivy-CN/flops
cd benchmark/
# C 版本(推荐)
gcc -std=c11 -O2 -w dhrystone.c -o dhrystone && ./dhrystone
gcc -std=c11 -O2 stream.c -o stream && ./stream
gcc -std=c11 -O2 -lm fft_bench.c -o fft_bench && ./fft_bench -N 65536
gcc -std=c11 -O2 -lm nbody.c -o nbody && ./nbody -N 200 -s 10
# Python 版本(对比语言效率)
python3 dhrystone.py
python3 stream.py # 需要 NumPy
python3 fft_bench.py -N 16384
python3 nbody.py -N 200
# 一键跑全部(回到项目根目录)
python3 run_all_benchmarks.py
python3 run_all_benchmarks.py --json # JSON 输出,方便挂进 CI

参考文献
- Dhrystone: Weicker, R.P. "Dhrystone: A Synthetic Systems Programming Benchmark." Communications of the ACM, 27(10):1013–1030, 1984.
- STREAM: McCalpin, J.D. "Memory Bandwidth and Machine Balance in Current High Performance Computers." IEEE CS TCCA Newsletter, Dec 1995. https://www.cs.virginia.edu/stream/
- FFT: Cooley, J.W. and Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." Mathematics of Computation, 19(90):297–301, 1965.
- N-body (O(N²)): von Hoerner, S. Zeitschrift für Astrophysik, 50:184, 1960. / Barnes, J. and Hut, P. "A Hierarchical O(N log N) Force-Calculation Algorithm." Nature, 324:446–449, 1986.
预览时标签不可点