如何在Rust中对程序进行基准测试?

163

在 Rust 中是否可以进行程序基准测试?如果可以,如何进行?例如,我该如何以秒为单位获取程序的执行时间?

9个回答

141

两年后可能值得注意的是(帮助任何在这个页面上偶然发现的未来Rust程序员),现在有工具可以作为测试套件的一部分对Rust代码进行基准测试。

(来自下面的指南链接)使用#[bench]属性,可以使用标准的Rust工具对代码中的方法进行基准测试。

extern crate test;
use test::Bencher;

#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
    b.iter(|| {
        // Use `test::black_box` to prevent compiler optimizations from disregarding
        // Unused values
        test::black_box(range(0u, 1000).fold(0, |old, new| old ^ new));
    });
}

对于命令 cargo bench,它的输出大致如下:

running 1 test
test bench_xor_1000_ints ... bench:       375 ns/iter (+/- 148)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured

链接:


15
显然,这将被淘汰。我仍在寻找应该使用什么。 - Cogman
1
你为什么认为会发生什么事情,具体是什么? - carols10cents
3
书籍的链接现在应指向所谓的“夜间版书籍”:https://doc.rust-lang.org/nightly/unstable-book/library-features/test.html。 - carols10cents
19
请注意,Bencher 的当前版本被标记为不稳定并且无法在稳定版 Rust 中使用。目前仅在 nightly 版本中可用。 - MaxB
3
更新一下,经过很长时间,#[bench]仍被认为是不稳定的,并且有传言称它将被弃用。截至目前,我建议使用criterion。如果你想了解更多关于#[bench]的内容,我建议从(我认为的) 第一个跟踪此问题的线程开始进入兔子洞。 - TDiblik

137

要在不添加第三方依赖项的情况下测量时间,您可以使用std::time::Instant


fn main() {
    use std::time::Instant;
    let now = Instant::now();

    // Code block to measure.
    {
        my_function_to_measure();
    }

    let elapsed = now.elapsed();
    println!("Elapsed: {:.2?}", elapsed);
}

2
它是否像 time 库的 precise_time_ns 一样精确? - kolen
5
请注意,您也可以通过其 Debug 实现简单地输出 Duration。例如:println!("{:.2?}", elapsed) - Lukas Kalbertodt
3
为什么 my_function_to_measure(); 要在自己的代码块(用 { } 包围)中?这是必要的吗? - pt1
1
@pt1 这并不是为了计时,而是为了清晰起见添加的。它表明该块中的任何内容都与时间测量分开(已更新为注释)。 - ideasman42

72

有{{几种}}方法可以对您的Rust程序进行基准测试。对于大多数真实的基准测试,您应该使用适当的基准测试框架,因为它们有助于解决一些易于出错的问题(包括统计分析)。请还要阅读位于底部的“为什么编写基准测试很难”部分!


快速简便:标准库中的InstantDuration

要快速检查代码运行时间,可以使用std::time中的类型。该模块相对较小,但对于简单的时间测量来说已经足够。应该使用Instant而不是SystemTime,因为前者是单调递增的时钟,而后者不是。例如(Playground):

use std::time::Instant;

let before = Instant::now();
workload();
println!("Elapsed time: {:.2?}", before.elapsed());

标准库中 Instant 的底层平台特定实现 在文档中 进行了说明。简而言之: 目前(也许永远)您可以假设它使用平台提供的最佳精度(或非常接近)。从我的测量和经验来看,这通常大约为20 ns。

如果 std::time 对您的情况没有足够的功能,则可以查看chrono。但是,对于测量持续时间,您不太可能需要该外部 crate。


使用基准测试框架

使用框架通常是一个好主意,因为它们试图防止您犯常见错误。

Rust的内置基准测试框架(仅限nightly版本)

Rust有一个方便的内置基准测试功能,但不幸的是截至2019-07仍不稳定。您必须向函数添加 #[bench] 属性,并使其接受一个&mut test :: Bencher参数:

#![feature(test)]

extern crate test;
use test::Bencher;

#[bench]
fn bench_workload(b: &mut Bencher) {
    b.iter(|| workload());
}

执行cargo bench将输出:
running 1 test
test bench_workload ... bench:      78,534 ns/iter (+/- 3,606)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out

准则

criterion 是一个框架,可以在稳定的环境下运行,但它比内置解决方案更为复杂。它执行更复杂的统计分析,提供更丰富的 API,生成更多信息,甚至可以自动生成图表。

有关如何使用 Criterion 的更多信息,请参见"快速入门"部分


为什么编写基准测试很难

编写基准测试时存在许多陷阱。一个错误可能会使您的基准测试结果毫无意义。以下是一些重要但常常被忽视的要点:

  • 进行优化编译: rustc -O3cargo build --release。使用cargo bench执行基准测试时,Cargo会自动启用优化。这一步非常重要,因为优化和未优化的Rust代码之间通常存在很大的性能差异。

  • 重复工作负载: 只运行一次工作负载几乎总是无用的。有许多因素会影响你的计时: 整个系统负载、操作系统执行任务、CPU限制、文件系统缓存等等。因此,尽可能地重复你的工作负载。例如,Criterion至少运行每个基准测试5秒钟(即使工作负载只需要几纳秒)。然后可以分析所有测量时间,平均值和标准偏差是标准工具。

  • 确保你的基准测试没有被完全删除: 基准测试本质上是非常人为的。通常,你只想测量持续时间,而不检查工作负载的结果。然而,这意味着一个好的优化器可能会删除整个基准测试,因为它没有副作用(除了时间的流逝)。因此,为了欺骗优化器,你必须以某种方式使用你的结果值,使得你的工作负载无法被删除。一种简单的方法是打印结果。更好的解决方案是像black_box这样的东西。这个函数基本上从LLVM中隐藏一个值,因为LLVM不能知道该值将会发生什么。什么都不会发生,但LLVM不知道。这就是重点。

    良好的基准测试框架在几种情况下使用块框。例如,对于iter方法(内置和Criterion Bencher都适用)给定的闭包可以返回一个值。该值会自动传递给black_box

  • 注意常量值: 与上面的观点类似,如果在基准测试中指定常量值,优化器可能会针对该值生成代码。在极端情况下,整个工作负载可能会折叠成一个单一的常量,这意味着你的基准测试是无用的。通过black_box传递所有常量值,以避免LLVM进行过度优化。

  • 注意测量开销: 测量持续时间本身需要时间。这通常只需要几十纳秒,但可能会影响你的测量时间。因此,对于所有快于几十纳秒的工作负载,你不应该单独测量每个执行时间。你可以执行你的工作负载100次,并测量所有100次执行的时间。将其除以100即可得到平均单次时间。上面提到的基准测试框架也使用了这个技巧。Criterion还有一些方法来测量具有副作用(如改变某些东西)的非常短的工作负载。

  • 许多其他事情: 不幸的是,我不能在这里列出所有的困难。如果你想编写严肃的基准测试,请阅读更多在线资源。


Instant 的文档说明时间不保证稳定,这意味着它不能可靠地作为计时器来查看某些操作所需的时间。使用类似于 Linux 的 clock_gettimeCLOCK_MONOTONIC_RAW 的接口会更好。 - nmichaels
1
“Idiomatic way of performance evaluation?”(https://dev59.com/8lIH5IYBdhLWcg3wR7pm)提到了一些其他的失败陷阱,包括在第一次通过数组时出现的软页故障开销。此外,CPU跳转到最大频率的延迟,这是与在最大涡轮运行一段时间后限制相反的问题。 - Peter Cordes
before.elapsed() 返回什么?秒,毫秒,纳秒?除此之外,这是一个很好的答案! - BitTickler
1
@BitTickler,你所提到的代码片段旁边有Instant的文档链接。在那里你可以看到elapsed返回的是一个自定义类型Duration,而不仅仅是一个浮点数或类似的东西。发布的代码(使用{:?}进行格式化)还会以方便的单位输出该持续时间,具体取决于持续时间的大小。 - Lukas Kalbertodt
1
@BitTickler,你所提到的代码片段旁边有Instant的文档链接。在那里你可以看到elapsed返回的是Duration,一个自定义类型,而不仅仅是一个浮点数或类似的东西。发布的代码(使用{:?}进行格式化)还以方便的单位输出了该持续时间,具体取决于持续时间的大小。 - undefined

55
如果您只是想计时代码,可以使用time包。然而,time已被弃用。推荐使用chrono替代。

time = "*"添加到您的Cargo.toml文件中。

添加

extern crate time;
use time::PreciseTime;

在你的主函数之前

let start = PreciseTime::now();
// whatever you want to do
let end = PreciseTime::now();
println!("{} seconds for whatever you did.", start.to(end));

完整示例

Cargo.toml

[package]
name = "hello_world" # the name of the package
version = "0.0.1"    # the current version, obeying semver
authors = [ "you@example.com" ]
[[bin]]
name = "rust"
path = "rust.rs"
[dependencies]
rand = "*" # Or a specific version
time = "*"

rust.rs

extern crate rand;
extern crate time;

use rand::Rng;
use time::PreciseTime;

fn main() {
    // Creates an array of 10000000 random integers in the range 0 - 1000000000
    //let mut array: [i32; 10000000] = [0; 10000000];
    let n = 10000000;
    let mut array = Vec::new();

    // Fill the array
    let mut rng = rand::thread_rng();
    for _ in 0..n {
        //array[i] = rng.gen::<i32>();
        array.push(rng.gen::<i32>());
    }

    // Sort
    let start = PreciseTime::now();
    array.sort();
    let end = PreciseTime::now();

    println!("{} seconds for sorting {} integers.", start.to(end), n);
}

2
time crate 现在显然已经被弃用。 - nbro
3
如果你想寻找取代time crate的解决方案,那么chrono crate可能是你需要的。不要将time crate与标准库中的std::time模块混淆。 - ElazarR
11
有点混淆:第一段提到应该使用chrono,但下面的代码似乎仍在使用time - bluenote10

19

这个回答已经过时了!在基准测试方面,time crate与std::time相比没有任何优势。请参阅下面的答案获取最新信息。


您可以尝试使用time crate来计时程序中的各个组件。


1
我特别喜欢 precise_time_nsprecise_time_s - Eric Holk
@nbro 不用太久!全面重写正在进行中。 - jhpratt
2
time crate现已发布0.2.0版本,不再被弃用。然而,据我所知,在测量基准时,它与std::time相比没有任何优势。因此,我更喜欢保留顶部的“此答案已过时”警告。下面有许多更好的答案。 - Lukas Kalbertodt
2
@LukasKalbertodt 我是新发布的 time crate 的作者。特别是考虑到内置的(尽管是夜间版)API,没有理由使用它进行基准测试。 - jhpratt

14

无论使用哪种编程语言,快速找出程序的执行时间的方法是在命令行上运行 time prog。例如:

~$ time sleep 4

real    0m4.002s
user    0m0.000s
sys     0m0.000s

最有趣的度量通常是user,它度量程序实际完成的工作量,而不管系统中正在发生什么(sleep对基准测试来说非常无聊)。real度量实际经过的时间,sys度量操作系统代表程序完成的工作量。


1
假设你只计时整个程序的执行时间,但有时你也想计时特定的步骤,尤其是如果程序是交互式的。 - ideasman42

5

目前,以下Linux函数没有任何接口:

  • clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts)
  • getrusage
  • times(manpage:man 2 times

在Linux上测量Rust程序的CPU时间和热点的可用方法包括:

  • /usr/bin/time program
  • perf stat program
  • perf record --freq 100000 program; perf report
  • valgrind --tool=callgrind program; kcachegrind callgrind.out.*

perf reportvalgrind的输出取决于程序中调试信息的可用性。可能无法正常工作。


2

我为此创建了一个小型的箱子(measure_time),它记录或打印作用域结束时的时间。

#[macro_use]
extern crate measure_time;
fn main() {
    print_time!("measure function");
    do_stuff();
}

1
另一种测量执行时间的解决方案是创建一个自定义类型,例如一个结构体,并为其实现Drop特质。
例如:
struct Elapsed(&'static str, std::time::SystemTime);

impl Drop for Elapsed {
    fn drop(&mut self) {
        println!(
            "operation {} finished for {} ms",
            self.0,
            self.1.elapsed().unwrap_or_default().as_millis()
        );
    }
}

impl Elapsed {
    pub fn start(op: &'static str) -> Elapsed {
        let now = std::time::SystemTime::now();

        Elapsed(op, now)
    }
}

并在某些函数中使用它:

fn some_heavy_work() {
  let _exec_time = Elapsed::start("some_heavy_work_fn");
  
  // Here's some code. 
}

当函数结束时,将调用_exec_time的drop方法并打印消息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接