写基准测试比想象中难:优化器会删掉没人用的计算,甚至把整个循环换成公式。这篇用真实的 cargo 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 的耗时)。