Rust 中的基准测试程序

如何在 Rust 中对程序进行基准测试?例如,如何得到以秒为单位的程序执行时间?

62582 次浏览

不管实现语言是什么,找出程序执行时间的一个快速方法是在命令行上运行 time prog。例如:

~$ time sleep 4


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

最有趣的度量方法通常是 user,它度量程序实际完成的工作量,而不管系统中发生了什么(sleep是一个相当枯燥的程序,需要进行基准测试)。real测量实际运行的时间,而 sys测量操作系统代表程序完成的工作量。

这个答案已经过时了!在基准测试方面,time板条箱不比 std::time有任何优势。有关最新信息,请参阅下面的答案。


您可以尝试使用 时间板条箱对程序中的各个组件计时。

目前,还没有与下列 Linux 函数的接口:

  • clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts)
  • getrusage
  • times(手册编号: 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的输出取决于程序中调试信息的可用性。它可能不工作。

可能值得注意的是,两年后(为了帮助任何未来的 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

相关网址:

如果您只是想计时一段代码,您可以使用 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));

完整的例子

货物,汤姆

[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 = "*"

生锈了

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);
}

为了在不添加第三方依赖关系的情况下测量时间,可以使用 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);
}

我为此创建了一个小板条箱(测量时间) ,它记录或打印到作用域结束的时间。

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

好几种方式为您的 Rust 程序做基准测试。对于大多数真正的基准测试,您应该使用适当的基准测试框架,因为它们有助于处理一些容易搞砸的事情(包括统计分析)。也请阅读最下面的“为什么写基准很难”部分!


快速简单: InstantDuration从标准库

要快速检查一段代码的运行时间,可以使用 std::time中的类型。这个模块相当小,但是对于简单的时间测量来说是很好的。您应该使用 Instant而不是 SystemTime,因为前者是一个单调增长的时钟,而后者不是。例子(游乐场) :

use std::time::Instant;


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

Std 的 Instant的底层平台特定的实现被指定为 在文件中。简而言之: 目前(可能永远) ,您可以假设它使用平台能够提供的最佳精度(或者非常接近它的东西)。根据我的测量和经验,这通常大约是20毫微秒。

如果 std::time没有为你的案例提供足够的功能,你可以看看 chrono。然而,为了测量持续时间,您不太可能需要外部板条箱。


使用基准测试框架

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

Rust 的内置基准测试框架(仅限夜间)

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方法的闭包(对于内置方法和 CriterionBencher)都可以返回一个值。该值将自动传递到 black_box

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

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

  • 很多其他的事情 : 遗憾的是,我不能在这里列出所有的困难。如果你想写严重的基准,请阅读更多的在线资源。

衡量执行时间的另一种解决方案是创建一个自定义类型,例如,一个结构并为其实现 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 方法并打印消息。