只测 FLOPS 还不够——四个经典 CPU 测试基准

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 测试的是三个维度的组合:

  1. 复数乘加:每个蝴蝶(butterfly)操作包含 4 次实数乘法 + 6 次实数加法。如果 CPU 有 FMA(融合乘加)指令,编译器可以在这里大显身手
  2. 跨步访存:FFT 的每一层分解,数组访问间距以 2 的幂次变化——从相邻到跨越半个数组。这折磨着缓存预取器
  3. 三角函数评估:每个蝶形需要 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


参考文献

  1. Dhrystone: Weicker, R.P. "Dhrystone: A Synthetic Systems Programming Benchmark." Communications of the ACM, 27(10):1013–1030, 1984.
  2. 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/
  3. 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.
  4. 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.

预览时标签不可点