Rust-开发必备-性能调优-Benchmark基准测试

127 阅读6分钟

背景

性能调优通常是开发中无法绕开的问题。除了对系统资源数据进行分析外,在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]的测试用例全部执行一遍,得到所有的执行结果。

image.png

执行cargo bench命令:

cargo bench

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

image.png

其中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

编写基准测试的建议

  1. 将设置代码移到 iter 循环之外;只将你想要测量的部分放在里面。
  2. 使代码在每次迭代时执行“相同的操作”;不要累积或改变状态。(从而提高结果的可重复性,减小测试结果的误差)
  3. 使外部函数也具有幂等性;基准测试运行器可能会多次运行它。
  4. 使内部 iter 循环简短且快速,这样基准测试运行速度就快,并且校准器可以在精细分辨率下调整运行长度。
  5. 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

本地复现结果:

image.png

原因是当编译器识别出这个函数没有外部影响时,就会将其优化掉。

方法一

基准测试运行器为了避免这种问题,提供了两种解决方法:一种方法是,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)
    });
}

运行结果:

image.png

方法二

另一种选择是调用通用的 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)
    });
}

结果:

image.png

不过需要注意的是,即使使用上述任何一种方法,优化器仍可能以非预期的方式修改我们的测试用例

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

image.png

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

运行结果:

image.png

     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

信息来源于官方文档。