背景
性能调优通常是开发中无法绕开的问题。除了对系统资源数据进行分析外,在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]
表明该用例是一个基准测试用例。
#![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
命令:
cargo test
得到如下运行结果:通过cargo test
会将当前项目中#[test]
(这里我们称为功能测试)及#[bench]
的测试用例全部执行一遍,得到所有的执行结果。
执行cargo bench
命令:
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
循环中的代码执行一些简单操作,以帮助确定性能提升(或下降)的情况。
如何避免基准测试被优化器干扰
启用优化编译选项的基准测试可能会被优化器大幅度改变测试结果,导致基准测试无法给出预期的测试结果。这里我们采用官方的例子说明该问题。
#![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
,最终会得到这样的结果:
文档中结果:
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
调用来实现这一点,如下所示:
#![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
函数,该函数对于优化器来说是一个不透明的 “黑盒”,因此会迫使优化器将任何参数视为已使用。
#![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
在前面的时候,我们提到cargo bench
当前只能在nightly
版本当中使用。如果我们需要一个在stable
环境当中可以使用的基准测试工具,这个时候就不得不提到Criterion.rs。
Criterion.rs
是一个由统计数据驱动的微基准测试工具。基准测试会在每次运行时收集并存储统计信息,不仅能够衡量优化效果,还能自动检测性能衰退情况。
Criterion.rs
有以下特性:
- 统计:统计分析检测自上次基准测试运行以来性能及变化了多少
- 图表化:使用gnuplot生成基准测试结果的详细图表
- 兼容stable:在不安装
nightly
的情况下对你的代码进行基准测试
示例
首先,我们需要在Cargo.toml
中配置项目:
[dev-dependencies]
criterion = { version = "0.5.1", features = ["html_reports"] }
[[bench]]
name = "my_benchmark"
harness = false
我们仍然对fn fibonacci(n: u64) -> u64
进行测试。在项目中创建./benches/my_benchmark.rs
。
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 => 1,
1 => 1,
n => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
仍然运行cargo bench
命令:
cargo bench
运行结果:
Running benches\my_benchmark.rs (target\release\deps\my_benchmark-6d9f09bf4edc9105.exe)
Gnuplot not found, using plotters backend
fib 20 time: [16.637 µs 16.898 µs 17.163 µs]
Found 12 outliers among 100 measurements (12.00%)
8 (8.00%) high mild
4 (4.00%) high severe
其中fib 20 time: [16.637 µs 16.898 µs 17.163 µs]
显示了此基准测试每次迭代测量时间的置信区间。左右两侧的值分别表示置信区间的下限和上限,而中间的值则表示Criterion.rs
对基准测试例程每次迭代所花费时间的最佳估计。
Found 12 outliers among 100 measurements (12.00%) 8 (8.00%) high mild 4 (4.00%) high severe
显示了Criterion.rs
的异常值报告:在100次测量中,有12次异常值,其中8次轻度偏高,4次严重偏高。
Ref
信息来源于官方文档。