bigywater.tech
Rust · 159

Benchmark

写基准测试比想象中难:优化器会删掉没人用的计算,甚至把整个循环换成公式。这篇用真实的 cargo bench 把这些陷阱一个个量出来,并看汇编验证优化器到底做了什么。

0.29 对 1,719 ns
同一个 mix(1000):没有 black_box 与有 black_box 的基准结果
15 条指令
sum_to 的汇编:平方和被优化成闭式公式,没有循环
5 次
同一基准重复运行次数,用来看波动

#[bench] 的输出

实测 · 本机执行输出
  test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  test clone_only_1000           ... bench:          69.10 ns/iter (+/- 40.60)
  test mix_no_blackbox           ... bench:           0.29 ns/iter (+/- 0.02)
  test mix_with_blackbox         ... bench:       1,718.61 ns/iter (+/- 35.63)
  test sort_1000                 ... bench:       6,032.50 ns/iter (+/- 239.85)
  test sum_squares_no_blackbox   ... bench:           0.92 ns/iter (+/- 0.02)
  test sum_squares_with_blackbox ... bench:           0.96 ns/iter (+/- 0.02)
  test result: ok. 0 passed; 0 failed; 0 ignored; 6 measured; 0 filtered out; finished in 1.34s

#[bench] 需要 nightly 特性(这里用 RUSTC_BOOTSTRAP=1);更完善的统计方案见Criterion 性能测试。

死代码消除与闭式公式

实测 · 本机执行输出
  mix(1000)(循环里 1000 次 xorshift,不能折叠成公式):没有 black_box 0.29 ns/iter;有 black_box 1718.61 ns/iter(约 5926 倍)
  sum_to(1000)(平方和,即使加了 black_box 也只有 0.96 ns/iter —— 远低于做 1000 次乘加所需的时间,说明优化器把循环变成了闭式公式)
  汇编里 sum_to:15 条指令,1 条跳转到本函数标签
  sum_to 的全部指令: cbz	x0, LBB1_2 ; sub	x8, x0, #1 ; sub	x9, x0, #2 ; umulh	x10, x8, x9 ; mul	x9, x8, x9 ; sub	x11, x0, #3 ; mul	x11, x9, x11 ; lsr	x11, x11, #1 ; mov	x12, #6148914691236517205 ; movk	x12, #21846 ; extr	x9, x10, x9, #1 ; add	x9, x9, x9, lsl #1 ; add	x8, x8, x9 ; madd	x0, x11, x12, x8 ; ret
  汇编里 mix:44 条指令,6 条跳转到本函数标签

迭代器链的汇编比较见Iterator 链的性能。

波动

实测 · 本机执行输出
  mix_with_blackbox: ['1,731.81 ns/iter (+/- 128.39)', '1,717.88 ns/iter (+/- 15.88)', '1,714.40 ns/iter (+/- 60.46)', '1,759.62 ns/iter (+/- 115.12)', '1,717.21 ns/iter (+/- 72.56)']
  sort_1000: ['5,995.41 ns/iter (+/- 287.26)', '6,049.97 ns/iter (+/- 90.80)', '6,148.76 ns/iter (+/- 286.57)', '6,036.92 ns/iter (+/- 264.09)', '6,238.17 ns/iter (+/- 445.72)']

准备工作也计入耗时

实测 · 本机执行输出
  sort_1000 6032.5 ns/iter(含 clone 一个 1000 元素的 Vec)  clone_only_1000 69.1 ns/iter  → 排序本身约 5963.4 ns,占 99%

稳定版自己写

实测 · 本机执行输出
  sum_to(1000) 稳定版             1.4 ns/iter(迭代 67108864 次,5 轮取最小)
  mix(1000) 稳定版             1708.8 ns/iter(迭代 32768 次,5 轮取最小)
  空闭包(测量开销)                    0.3 ns/iter(迭代 268435456 次,5 轮取最小)

本系列里的微基准都按这个方式写,每一轮的结果都经过 black_box(如果漏掉,取多轮最小值时会选中被优化器删掉计算的那一轮,得到接近 0 的耗时)。

分步演示

Benchmark
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。#[bench] 通过 RUSTC_BOOTSTRAP=1 使用
    • 没有验证:统计显著性(多次运行的置信区间)、CPU 频率与散热对结果的影响、多线程基准、iai 之类按指令数计数的基准、不同优化级别下闭式公式是否仍成立
    京ICP备2026067502号-1京公网安备11010502063487号