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

信息来源于官方文档。

相关推荐
pumpkin8451410 小时前
Rust 调用 C 函数的 FFI
c语言·算法·rust
蜗牛沐雨10 小时前
警惕 Rust 字符串的性能陷阱:`chars().nth()` 的深坑与高效之道
开发语言·后端·rust
susnm19 小时前
Dioxus 与数据库协作
前端·rust
羊八井19 小时前
类型、分类定义时使用 type 还是 kind ?
rust·typescript·代码规范
Source.Liu1 天前
【unitrix】 4.18 类型级二进制数加法实现解析(add.rs)
rust
KENYCHEN奉孝1 天前
Rust征服字节跳动:高并发服务器实战
服务器·开发语言·rust
明天好,会的2 天前
跨平台ZeroMQ:在Rust中使用zmq库的完整指南
开发语言·后端·rust
寻月隐君2 天前
Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx)
后端·rust·github
芳草萋萋鹦鹉洲哦2 天前
【vue3+tauri+rust】如何实现下载文件mac+windows
windows·macos·rust
寻月隐君2 天前
Rust 异步编程实践:从 Tokio 基础到阻塞任务处理模式
后端·rust·github