如何在 Rust 中对程序进行基准测试?例如,我如何以秒为单位获得程序的执行时间?
9 回答
两年后可能值得注意(以帮助任何偶然发现此页面的未来 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
链接:
要在不添加第三方依赖项的情况下测量时间,您可以使用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);
}
如果你只是想为一段代码计时,你可以使用time
crate。不过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));
完整示例
货运.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 = "*"
锈.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);
}
有几种方法可以对你的 Rust 程序进行基准测试。对于大多数真正的基准测试,您应该使用适当的基准测试框架,因为它们有助于解决一些容易搞砸的事情(包括统计分析)。另请阅读最底部的“为什么编写基准测试很难”部分!
快速简单:Instant
来自Duration
标准库
要快速检查一段代码运行了多长时间,您可以使用std::time
. 该模块相当小,但它适用于简单的时间测量。您应该使用Instant
代替,SystemTime
因为前者是单调递增的时钟,而后者不是。示例(游乐场):
use std::time::Instant;
let before = Instant::now();
workload();
println!("Elapsed time: {:.2?}", before.elapsed());
文档Instant
中指定了标准的底层平台特定实现。简而言之:目前(并且可能永远)您可以假设它使用平台可以提供的最佳精度(或非常接近它的东西)。根据我的测量和经验,这通常约为 20 ns。
如果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
标准
cratecriterion
是一个在 stable 上运行的框架,但它比内置解决方案要复杂一些。它进行更复杂的统计分析,提供更丰富的 API,生成更多信息,甚至可以自动生成图表。
有关如何使用 Criterion 的更多信息,请参阅“快速入门”部分。
为什么编写基准测试很难
编写基准测试时有很多陷阱。一个错误可能会使您的基准测试结果毫无意义。以下是重要但通常被遗忘的要点的列表:
使用优化编译:
rustc -O3
或cargo 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 也有一些方法可以测量具有副作用的非常短的工作负载(比如改变某些东西)。
许多其他事情:遗憾的是,我无法在这里列出所有困难。如果您想编写严肃的基准测试,请阅读更多在线资源。
无论实现语言如何,找出程序执行时间的一种快速方法是time prog
在命令行上运行。例如:
~$ time sleep 4
real 0m4.002s
user 0m0.000s
sys 0m0.000s
最有趣的测量通常是user
,它测量程序完成的实际工作量,而不管系统中发生了什么(这sleep
是一个非常无聊的基准程序)。real
测量经过的实际时间,并sys
测量操作系统代表程序完成的工作量。
目前,以下任何 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 report
和的输出valgrind
取决于程序中调试信息的可用性。它可能不起作用。
我为此创建了一个小箱子(measure_time),它记录或打印直到范围结束的时间。
#[macro_use]
extern crate measure_time;
fn main() {
print_time!("measure function");
do_stuff();
}
测量执行时间的另一种解决方案是创建自定义类型,例如,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 方法并打印消息。