为测试目的计时函数调用的最简单方法是什么?

What is the easiest way to time a function call for testing purposes?

所以我对 Rust 还是有点陌生​​,但是来自 Python 我发现这种情况一般来说非常令人困惑。

我喜欢 Python 因为如果你想为一段代码或只是一个函数调用计时,这很容易:

print(timeit('a = "hee hee la le dah"; my_awesome_fn()', number = 1_000, globals=globals()))

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

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

所以我只是好奇如何在 cargo 中使用 tests 来测试 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!");
}

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

免责声明:我从未使用过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
}

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

fn main() {
  let x = timeit(|| my_expensive_function());
}

但是,如果您试图了解一个函数所花费的时间以达到性能优化的目的,这种方法可能过于粗糙。

The problem is that I know nothing about advanced math and statistics

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

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

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

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

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

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

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

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

常见的“陷阱”是:

  • rustc一般可以识别“无用”代码,直接跳过计算。 black_box 函数可用于向优化器隐藏某些数据的含义,尽管它并非没有自己的开销
  • 以类似的方式,LLVM 做了一些与多项式 for example 相关的稍微诡异的优化。您可能会发现您的函数调用被优化为 constant/simple 算术。在某些情况下,这很棒!您以 LLVM 可以将其简化为微不足道的方式编写函数。在其他情况下,您现在只是在 CPU 上对乘法指令进行基准测试,这不太可能是您想要的。用你最好的判断力
  • 基准测试错误 - 有些东西比其他东西贵得多,对于具有 python 背景的人来说可能看起来很奇怪。例如,克隆 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()));
}

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

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