背景
性能调优通常是开发中无法绕开的问题。除了对系统资源数据进行分析外,在Rust中我们还可以通过工具进行基准测试,帮助我们分析调优的结果。
cargo bench
cargo bench
是官方自带的基准测试工具。但该功能当前只能在nightly
版本中才能使用 (印象中已经持续几年了),在Unstable Book中可以查找到对应内容。
以下是一个基准测试示例:对fn fibonacci(n: u64) -> u64
进行测试。首先我们需要通过#![feature(test)]
来启用这个功能,然后通过extern crate test
导入test
。如示例中所示,我们编写了两个测试用例#[test]
属性的test_fibonacci
和#[bench]
属性的bench_fibonacci
,#[bench]
表明该用例是一个基准测试用例。
rust
#![feature(test)]
extern crate test;
pub fn fibonacci(n: u64) -> u64 {
match n {
0 => 1,
1 => 1,
n => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_fibonacci() {
assert_eq!(super::fibonacci(20), 10946);
}
#[bench]
fn bench_fibonacci(b: &mut test::Bencher) {
b.iter(|| {
super::fibonacci(20)
});
}
}
基准测试将携带&mut Bencher
,通过iter
方法接受一个闭包,闭包中运行我们想要的基准测试代码。
执行cargo test
命令:
shell
cargo test
得到如下运行结果:通过cargo test
会将当前项目中#[test]
(这里我们称为功能测试)及#[bench]
的测试用例全部执行一遍,得到所有的执行结果。

执行cargo bench
命令:
shell
cargo bench
得到如下运行结果:功能测试在基准测试中被忽略,输出基准测试的测试结果。基准测试相对cargo test
会花费更多时间,原因是Rust会多次运行基准测试,然后取平均值。

其中0.24 ns/iter
:表示 bench_fibonacci
函数平均每次迭代(执行)所花费的时间为 0.24 纳秒(ns)。(+/- 0.04)
:是测量结果的误差范围,单位与前面的时间单位一致,即纳秒。这意味着每次迭代的执行时间大约在 0.20 纳秒(0.24 - 0.04)到 0.28 纳秒(0.24 + 0.04)之间波动。
更多cargo bench
指令介绍可查看Cargo Book
编写基准测试的建议
- 将设置代码移到
iter
循环之外;只将你想要测量的部分放在里面。 - 使代码在每次迭代时执行"相同的操作";不要累积或改变状态。(从而提高结果的可重复性,减小测试结果的误差)
- 使外部函数也具有幂等性;基准测试运行器可能会多次运行它。
- 使内部
iter
循环简短且快速,这样基准测试运行速度就快,并且校准器可以在精细分辨率下调整运行长度。 - 让
iter
循环中的代码执行一些简单操作,以帮助确定性能提升(或下降)的情况。
如何避免基准测试被优化器干扰
启用优化编译选项的基准测试可能会被优化器大幅度改变测试结果,导致基准测试无法给出预期的测试结果。这里我们采用官方的例子说明该问题。
rust
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
(0..1000).fold(0, |old, new| old ^ new);
});
}
对示例执行cargo bench
,最终会得到这样的结果:
文档中结果:
shell
running 1 test
test bench_xor_1000_ints ... bench: 0 ns/iter (+/- 0)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
本地复现结果:

原因是当编译器识别出这个函数没有外部影响时,就会将其优化掉。
方法一
基准测试运行器为了避免这种问题,提供了两种解决方法:一种方法是,iter
方法接收的闭包可以返回一个任意值,这会迫使优化器认为结果被使用了,从而确保它不会完全删除计算过程。对于上面的示例,可以通过调整 b.iter
调用来实现这一点,如下所示:
rust
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
// Note lack of `;` (could also use an explicit `return`).
(0..1000).fold(0, |old, new| old ^ new)
});
}
运行结果:

方法二
另一种选择是调用通用的 test::black_box
函数,该函数对于优化器来说是一个不透明的 "黑盒",因此会迫使优化器将任何参数视为已使用。
rust
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
let n = test::black_box(1000);
(0..n).fold(0, |a, b| a ^ b)
});
}
结果:

不过需要注意的是,即使使用上述任何一种方法,优化器仍可能以非预期的方式修改我们的测试用例。
Criterion.rs
Ref
信息来源于官方文档。