出于测试目的对函数调用进行计时的最简单方法是什么?

rv.*_*tch 2 time performance-testing timeit rust

所以我对 Rust 还很陌生,但来自 Python 的我发现这种情况总体来说非常令人困惑。

我喜欢 Python,因为如果你想对一段代码或只是一个函数调用进行计时,它非常容易:

print(timeit('a = "hee hee la le dah"; my_awesome_fn()', number = 1_000, globals=globals()))
Run Code Online (Sandbox Code Playgroud)

然后只需调用python script.py,或者更好的是,只需使用 IDE 中的绿色“运行”按钮即可调用脚本。但我在 Rust 中找不到等效的功能。

我知道 Rust 生态系统中有一个称为基准测试的概念,并且有一些类似的库就是criterion为此目的而存在的。问题是我对高等数学和统计学一无所知(本质上可以把我当作一个无能的白痴),而且我怀疑我能否从这样的框架或工具中受益匪浅。

所以我只是好奇如何tests在 Cargo 中使用来测试 Rust 中的代码块,甚至更好,甚至是函数调用。

例如,假设我在 Rust 中有类似的函数,我想多次调用它,然后检查性能如何变化等:

pub fn my_awesome_fn() {
    trace!("getting ready to do something cool...");
    std::thread::sleep(std::time::Duration::from_millis(500));
    info!("finished!");
}
Run Code Online (Sandbox Code Playgroud)

我怎样才能简单地在 Rust 中计时这个函数my_awesome_fn?我想我正在寻找类似timeitpython 或类似的东西。理想情况下,它应该是直接使用的,并假设我对我正在做的事情一无所知。我很好奇是否有一个现有的库或框架可以用于此目的。

cam*_*024 11

免责声明:我从未使用过timeit

一个非常快速的答案解决方案是编写一个如下函数:

fn timeit<F: Fn() -> T, T>(f: F) -> T {
  let start = SystemTime::now();
  let result = f();
  let end = SystemTime::now();
  let duration = end.duration_since(start).unwrap();
  println!("it took {} seconds", duration.as_secs());
  result
}
Run Code Online (Sandbox Code Playgroud)

您可以使用它来“包装”另一个函数调用:

fn main() {
  let x = timeit(|| my_expensive_function());
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您试图了解函数出于性能优化的目的而花费的时间,则这种方法可能过于粗糙。

问题是我对高等数学和统计学一无所知

从某种意义上说,这可以说是 的主要优点之一criterion,它“抽象了数学”。

它使用统计方法让您更好地了解基准测试运行之间的差异是否是“随机性”的产物,或者每次运行的代码之间是否存在有意义的差异。

对于最终用户来说,它本质上是为您提供一份报告,说明“观察到显着变化”或“未观察到显着变化”。它的作用远不止于此,但要充分掌握其功能,可能值得阅读“假设检验”。

如果您可以使用 nightly Rust,您还可以使用#[bench]测试:

#![feature(test)]
extern crate test;

#[bench]
fn bench_my_func(b: &mut Bencher) {
  b.iter(|| my_func(black_box(100));
}
Run Code Online (Sandbox Code Playgroud)

你可以用它来运行cargo bench。这些比 更容易设置criterion,但做的有趣统计数据较少(即您必须自己做),但它们是一种非常“快速而肮脏”的方式来了解您的运行时代码。

警告一句,基准测试代码很难。您可能会对幕后实际发生的情况感到惊讶,并且您可能会发现自己对错误的事情进行了基准测试。

常见的“陷阱”是:

  • rustc通常可以识别“无用”代码,并简单地跳过计算它。该black_box函数可用于向优化器隐藏某些数据的含义,尽管它并非没有自身的开销
  • 同样,LLVM 做了一些与多项式相关的稍微令人毛骨悚然的优化。您可能会发现您的函数调用正在被优化为常量/简单算术。在某些情况下,这非常棒!您已经以 LLVM 可以将其简化为微不足道的方式编写了函数。在其他情况下,您现在只是对 CPU 上的乘法指令进行基准测试,这不太可能是您想要的。运用你的最佳判断
  • 对错误的东西进行基准测试——有些东西比其他东西贵得多,这对于有 python 背景的人来说可能看起来很奇怪。例如,克隆 a String(即使是非常短的字符)可能比查找第一个字符慢 2-3 个数量级。考虑以下:
fn str_len(s: String) -> usize {
  s.len()
}

#[bench]
fn bench_str_len(b: &mut Bencher) {
  let s = String::from("hello");  
  b.iter(|| str_len(s.clone()));
}
Run Code Online (Sandbox Code Playgroud)

因为String::clone涉及到堆分配,而s.len()只是字段访问,所以会主导结果。相反,如果str_len采用&str,它将变得更具代表性(尽管这是一个人为的案例)。

TLDR 请小心您的基准代码正在做什么。Rust Playground 的“查看程序集”工具(或 godbolt.org)是您的朋友。您不需要成为装配专家,但它可以帮助您了解幕后发生的事情