在 Rust 中是否可以进行程序基准测试?如果可以,如何进行?例如,我该如何以秒为单位获取程序的执行时间?
在 Rust 中是否可以进行程序基准测试?如果可以,如何进行?例如,我该如何以秒为单位获取程序的执行时间?
两年后可能值得注意的是(帮助任何在这个页面上偶然发现的未来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
库的 precise_time_ns
一样精确? - kolenDebug
实现简单地输出 Duration
。例如:println!("{:.2?}", elapsed)
。 - Lukas Kalbertodtmy_function_to_measure();
要在自己的代码块(用 { } 包围)中?这是必要的吗? - pt1有{{几种}}方法可以对您的Rust程序进行基准测试。对于大多数真实的基准测试,您应该使用适当的基准测试框架,因为它们有助于解决一些易于出错的问题(包括统计分析)。请还要阅读位于底部的“为什么编写基准测试很难”部分!
Instant
和Duration
要快速检查代码运行时间,可以使用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有一个方便的内置基准测试功能,但不幸的是截至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 -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还有一些方法来测量具有副作用(如改变某些东西)的非常短的工作负载。
许多其他事情: 不幸的是,我不能在这里列出所有的困难。如果你想编写严肃的基准测试,请阅读更多在线资源。
Instant
的文档说明时间不保证稳定,这意味着它不能可靠地作为计时器来查看某些操作所需的时间。使用类似于 Linux 的 clock_gettime
和 CLOCK_MONOTONIC_RAW
的接口会更好。 - nmichaelsbefore.elapsed()
返回什么?秒,毫秒,纳秒?除此之外,这是一个很好的答案! - BitTicklerInstant
的文档链接。在那里你可以看到elapsed
返回的是一个自定义类型Duration
,而不仅仅是一个浮点数或类似的东西。发布的代码(使用{:?}
进行格式化)还会以方便的单位输出该持续时间,具体取决于持续时间的大小。 - Lukas KalbertodtInstant
的文档链接。在那里你可以看到elapsed
返回的是Duration
,一个自定义类型,而不仅仅是一个浮点数或类似的东西。发布的代码(使用{:?}
进行格式化)还以方便的单位输出了该持续时间,具体取决于持续时间的大小。 - undefinedtime
包。然而,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);
}
precise_time_ns
和 precise_time_s
。 - Eric Holktime
crate现已发布0.2.0版本,不再被弃用。然而,据我所知,在测量基准时,它与std::time
相比没有任何优势。因此,我更喜欢保留顶部的“此答案已过时”警告。下面有许多更好的答案。 - Lukas Kalbertodt无论使用哪种编程语言,快速找出程序的执行时间的方法是在命令行上运行 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
(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 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方法并打印消息。