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

背景

性能调优通常是开发中无法绕开的问题。除了对系统资源数据进行分析外,在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

编写基准测试的建议

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

信息来源于官方文档。

相关推荐
用户099691880186 小时前
rust 获取 hugging face 模型 chat template
rust
受之以蒙6 小时前
Rust FFI实战指南:跨越语言边界的优雅之道
笔记·rust
勇敢牛牛_10 小时前
【Rust基础】使用Rocket从Token中提取用户信息
开发语言·rust·bootstrap·rocket
muyouking1118 小时前
4.Rust+Axum Tower 中间件实战:从集成到自定义
开发语言·中间件·rust
Source.Liu19 小时前
【TeamFlow】4.2 Yew库详细介绍
rust·yew
勇敢牛牛_19 小时前
【MRAG】使用RAG技术增强AI回复的实时性和准确性
rust·知识库·rag
techdashen21 小时前
性能比拼: Rust vs Zig vs Go
开发语言·golang·rust
muyouking111 天前
9.Rust+Axum 测试驱动开发与性能优化全攻略
驱动开发·性能优化·rust
阿阳热爱前端1 天前
BongoCat 桌宠全新升级!开源 × 跨平台,快来撸猫!
前端·rust·app