25 个版本 (12 个重大更改)

0.13.0 2024 年 8 月 19 日
0.12.1 2024 年 7 月 31 日
0.10.2 2024 年 1 月 26 日
0.9.0 2023 年 12 月 9 日
0.3.1 2023 年 3 月 13 日

#7性能分析 中排名

Download history 2718/week @ 2024-05-04 2306/week @ 2024-05-11 2760/week @ 2024-05-18 2886/week @ 2024-05-25 3159/week @ 2024-06-01 2734/week @ 2024-06-08 3162/week @ 2024-06-15 3828/week @ 2024-06-22 3805/week @ 2024-06-29 3751/week @ 2024-07-06 4071/week @ 2024-07-13 3279/week @ 2024-07-20 3993/week @ 2024-07-27 4813/week @ 2024-08-03 3801/week @ 2024-08-10 3570/week @ 2024-08-17

每月 16,618 次下载
22 个库 中使用

Apache-2.0 OR MIT

470KB
6K SLoC

iai-callgrind

Rust 的高精度和一致性的基准测试框架/工具

iai-callgrind 是一个基准测试框架/工具,主要使用 Valgrind 的 Callgrind 和其他 Valgrind 工具来提供对 Rust 代码的极高精度和一致的测量,非常适合在 CI 环境中运行。此外,iai-callgrind 还集成在 Bencher 中。

这个crate最初是伟大的 Iai crate 的分支,重写为使用 Valgrind 的 Callgrind 而不是 Cachegrind,但还增加了许多其他改进和功能。

目录

特性

  • 精度:高精度测量可让您可靠地检测代码中的非常小的优化
  • 一致性:iai-callgrind甚至可以在虚拟化的 CI 环境中准确地测量
  • 性能:由于Iai-Callgrind只执行基准测试一次,因此它通常比测量执行时间和墙时长的基准测试要快得多。
  • 回归:Iai-Callgrind报告基准测试运行之间的差异,使其容易发现详细性能回归和改进。您可以定义特定事件类型的限制,如果超过该限制则基准测试失败。
  • CPU和缓存分析:Iai-Callgrind在基准测试期间生成您的代码的Callgrind分析文件,因此您可以使用与Callgrind兼容的工具,如callgrind_annotate或可视化器kcachegrind来详细分析结果。
  • 内存分析:您可以使用Iai-Callgrind基准测试框架运行其他Valgrind工具,如DHAT:动态堆分析工具Massif:堆分析器。它们的分析文件存储在callgrind分析文件旁边,并准备好使用分析工具如dh_view.htmlms_print等进行分析。
  • 可视化:Iai-Callgrind能够从Callgrind输出格式创建常规和差异火焰图。
  • Valgrind客户端请求:在许多目标上支持零开销Valgrind客户端请求(与本地valgrind客户端请求开销相比)
  • 稳定兼容:无需安装夜间Rust即可基准测试您的代码

安装

为了使用Iai-Callgrind,您必须安装Valgrind。这意味着Iai-Callgrind不能在不支持Valgrind的平台中使用。

要开始使用Iai-Callgrind,请将以下内容添加到您的Cargo.toml文件中

[dev-dependencies]
iai-callgrind = "0.13.0"

为了能够运行基准测试,您还需要安装iai-callgrind-runner二进制文件,例如将其添加到您的$PATH中,例如

cargo install --version 0.13.0 iai-callgrind-runner

或者使用binstall

cargo binstall [email protected]

您还可以将二进制文件安装到其他位置,并将环境变量IAI_CALLGRIND_RUNNER指向iai-callgrind-runner二进制的绝对路径,如下所示

cargo install --version 0.13.0 --root /tmp iai-callgrind-runner
IAI_CALLGRIND_RUNNER=/tmp/bin/iai-callgrind-runner cargo bench --bench my-bench

在更新iai-callgrind库时,您还需要更新iai-callgrind-runner及其相反,否则基准测试运行器将以错误退出。否则,无需与iai-callgrind-runner交互,因为它只是实现细节。

由于iai-callgrind-runner版本必须与iai-callgrind库版本匹配,因此最好在CI中自动化此步骤。GitHub Actions CI中的一个作业步骤可能如下所示

- name: Install iai-callgrind-runner
  run: |
    version=$(cargo metadata --format-version=1 |\
      jq '.packages[] | select(.name == "iai-callgrind").version' |\
      tr -d '"'
    )
    cargo install iai-callgrind-runner --version $version

如果您想使用Valgrind客户端请求,则需要安装libclang(clang >= 5.0)。另请参阅bindgencc的要求。

iai-callgrind在运行基准测试时需要调试符号。您可以在多个位置配置配置文件,有关更多详细信息,请参阅下面的基准测试部分。

基准测试

iai-callgrind可用于基准测试库或二进制文件。库基准测试基准测试crates中的函数和方法,而二进制基准测试基准测试crates的可执行文件。不同的基准测试类型不能在同一个基准测试文件中混合,但为库和二进制基准测试提供不同的基准测试文件没有问题。有关更多信息,请参阅以下部分。

有关快速入门和基准测试库的示例,请参阅库基准测试部分,有关可执行文件,请参阅二进制基准测试部分。阅读docs

如上文所述,在安装部分中提到,需要启用调试符号来运行基准测试。例如,在你的~/.cargo/config或你的项目的Cargo.toml

[profile.bench]
debug = true

现在,您使用cargo bench运行的所有基准测试都包含调试符号。(也请参阅Cargo配置文件Cargo配置)。

如果您已更改release配置文件的此选项,则需要显式禁用类似于strip = true或其他配置选项以删除调试符号的设置,以使bench配置文件生效。例如

[profile.release]
strip = true

[profile.bench]
debug = true
strip = false

默认情况下,iai-callgrind使用Valgrind的缓存模拟打开(--cache-sim=yes)来运行所有基准测试,以便计算总CPU周期的估计值。有关更多信息,请参阅指标输出部分。但是,如果您想选择退出缓存模拟和估计周期的计算,您可以在基准测试中通过LibraryBenchmarkConfig(或BinaryBenchmarkConfig)轻松做到这一点。

use iai_callgrind::{LibraryBenchmarkConfig, main};
/* library benchmarks and groups */
main!(
    config = LibraryBenchmarkConfig::default().raw_callgrind_args(["--cache-sim=no"]);
    library_benchmark_groups = /* ... */
);

在命令行中使用环境变量

IAI_CALLGRIND_CALLGRIND_ARGS="--cache-sim=no" cargo bench

或使用参数

cargo bench -- --callgrind-args="--cache-sim=no"

要能够运行上述命令,可能需要一些额外的配置

库基准测试

如果您想微基准测试您的crate库的特定函数,请使用此方案。

重要默认行为

在运行库基准测试之前,会清除环境变量。如果您需要更改此行为,请查看配置部分。

快速入门

[[bench]]
name = "my_benchmark"
harness = false

添加到您的Cargo.toml文件中,然后在benches/my_benchmark.rs中创建一个具有相同name的文件,内容如下

use iai_callgrind::{main, library_benchmark_group, library_benchmark};
use std::hint::black_box;

fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 1,
        1 => 1,
        n => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[library_benchmark]
#[bench::short(10)]
#[bench::long(30)]
fn bench_fibonacci(value: u64) -> u64 {
    black_box(fibonacci(value))
}

library_benchmark_group!(
    name = bench_fibonacci_group;
    benchmarks = bench_fibonacci
);

main!(library_benchmark_groups = bench_fibonacci_group);

请注意,在基准测试函数上使用#[library_benchmark]是重要的。但是,现在不再需要使用inline(never)来注释基准测试函数。属性bench接受任何包含函数调用的表达式。下面的示例也可以工作,并且避免了在基准测试函数中设置代码,从而消除了传递toggle-collect参数给callgrind的需要

/* ... */

fn some_setup_func(value: u64) -> u64 {
    value + 10
}

#[library_benchmark]
#[bench::long(some_setup_func(30))]
fn bench_fibonacci(value: u64) -> u64 {
    black_box(fibonacci(value))
}

/* ... */

现在,您可以在项目根目录中运行此基准测试,使用cargo bench --bench my_benchmark,您应该会看到如下内容

test_lib_bench_readme_example_fibonacci::bench_fibonacci_group::bench_fibonacci short:10
  Instructions:                1733|N/A             (*********)
  L1 Hits:                     2359|N/A             (*********)
  L2 Hits:                        0|N/A             (*********)
  RAM Hits:                       2|N/A             (*********)
  Total read+write:            2361|N/A             (*********)
  Estimated Cycles:            2429|N/A             (*********)
test_lib_bench_readme_example_fibonacci::bench_fibonacci_group::bench_fibonacci long:30
  Instructions:            26214733|N/A             (*********)
  L1 Hits:                 35638617|N/A             (*********)
  L2 Hits:                        0|N/A             (*********)
  RAM Hits:                       4|N/A             (*********)
  Total read+write:        35638621|N/A             (*********)
  Estimated Cycles:        35638757|N/A             (*********)

此外,您还可以在target/iai中找到callgrind输出,如果您想使用类似callgrind_annotate的工具进一步调查,当再次运行相同的基准测试时,输出将报告当前运行和上次运行之间的差异。例如,如果您已更改了fibonacci函数,您可能会看到如下内容

test_lib_bench_readme_example_fibonacci::bench_fibonacci_group::bench_fibonacci short:10
  Instructions:                2804|1733            (+61.8003%) [+1.61800x]
  L1 Hits:                     3815|2359            (+61.7211%) [+1.61721x]
  L2 Hits:                        0|0               (No change)
  RAM Hits:                       2|2               (No change)
  Total read+write:            3817|2361            (+61.6688%) [+1.61669x]
  Estimated Cycles:            3885|2429            (+59.9424%) [+1.59942x]
test_lib_bench_readme_example_fibonacci::bench_fibonacci_group::bench_fibonacci long:30
  Instructions:            16201596|26214733        (-38.1966%) [-1.61803x]
  L1 Hits:                 22025878|35638617        (-38.1966%) [-1.61803x]
  L2 Hits:                        0|0               (No change)
  RAM Hits:                       3|4               (-25.0000%) [-1.33333x]
  Total read+write:        22025881|35638621        (-38.1966%) [-1.61803x]
  Estimated Cycles:        22025983|35638757        (-38.1965%) [-1.61803x]
更详细地介绍#[library_benchmark]属性

此属性必须存在于所有在 library_benchmark_group 中指定的基准函数上。然后,基准函数可以通过 #[bench]#[benches] 属性进行进一步注释。

use iai_callgrind::{library_benchmark, library_benchmark_group};

#[library_benchmark]
#[bench::first(21)]
fn my_bench(value: u64) -> u64 {
    // benchmark something
}

library_benchmark_group!(name = my_group; benchmarks = my_bench);

以下参数被接受:

  • config:接受一个 LibraryBenchmarkConfig
  • setup:一个全局设置函数,应用于所有后续的 #[bench]#[benches] 属性,除非被这些属性的 setup 参数覆盖。
  • teardown:与 setup 类似,但接受一个全局 teardown 函数。

关于 setup 参数的使用示例

use iai_callgrind::library_benchmark;

fn my_setup(value: u64) -> String {
     format!("{value}")
}

fn my_other_setup(value: u64) -> String {
     format!("{}", value + 10)
}

#[library_benchmark(setup = my_setup)]
#[bench::first(21)]
#[benches::multiple(42, 84)]
#[bench::last(args = (102), setup = my_other_setup)]
fn my_bench(value: String) {
    println!("{value}");
}

在此,具有 ID firstmultiple 的基准使用 my_setup 函数,而 last 使用 my_other_setup

关于 teardown 参数的简单示例

use iai_callgrind::library_benchmark;
use std::hint::black_box;

fn my_teardown(value: usize) {
     println!("The length of the input string was: {value}");
}

fn my_other_teardown(value: usize) {
     if value != 3 {
         panic!("The length of the input string was: {value} but expected it to be 3");
     }
}

#[library_benchmark(teardown = my_teardown)]
#[bench::first("1")]
#[benches::multiple("42", "84")]
#[bench::last(args = ("104"), teardown = my_other_teardown)]
fn my_bench(value: &str) -> usize {
    // Let's benchmark the `len` function
    black_box(value.len())
}

此示例与 --nocapture 选项配合良好(环境变量:IAI_CALLGRIND_NOCAPTURE,另请参阅显示基准的终端输出),因此您可以实际看到 my_teardown 函数的输出。

#[bench] 属性

基本结构为 #[bench::some_id(/* 参数 */)]。冒号后面的部分必须在同一个 #[library_benchmark] 中是唯一的。此属性接受以下参数:

  • args:一个元组,包含传递给基准函数的参数列表。如果只有一个参数,括号也必须存在(例如:#[bench::my_id(args = (10))])。
  • config:接受一个 LibraryBenchmarkConfig
  • setup:一个函数,它接受在 args 参数中指定的参数,并将它的返回值传递给基准函数。
  • teardown:一个函数,它接受基准函数的返回值。

如果没有其他参数除了 args,你只需将参数作为值列表传递。而不是使用 #[bench::my_id(args = (10, 20))],你也可以使用更短的 #[bench::my_id(10, 20)]

使用#[benches]属性同时指定多个基准测试

此属性接受与#[bench]属性相同的参数: argsconfigsetupteardown,以及额外的 file 参数。与 #[bench] 中的 args 参数相比,args 接受一个参数数组。

让我们从一个例子开始

use iai_callgrind::library_benchmark;
use std::hint::black_box;
use my_lib::bubble_sort;

fn setup_worst_case_array(start: i32) -> Vec<i32> {
    if start.is_negative() {
        (start..0).rev().collect()
    } else {
        (0..start).rev().collect()
    }
}

#[library_benchmark]
#[benches::multiple(vec![1], vec![5])]
#[benches::with_setup(args = [1, 5], setup = setup_worst_case_array)]
fn bench_bubble_sort_with_benches_attribute(input: Vec<i32>) -> Vec<i32> {
    black_box(bubble_sort(input))
}

通常,arguments 会直接传递给基准测试函数,如 #[benches::multiple(...)] 的情况。在 #[benches::with_setup(...)] 中,参数会传递给 setup 函数。上面的 #[library_benchmark] 几乎与以下相同,但更加简洁,特别是当传递给相同的 setup 函数很多值时。

use iai_callgrind::library_benchmark;
use std::hint::black_box;
use my_lib::bubble_sort;

#[library_benchmark]
#[bench::multiple_0(vec![1])]
#[bench::multiple_1(vec![5])]
#[bench::with_setup_0(setup_worst_case_array(1))]
#[bench::with_setup_1(setup_worst_case_array(5))]
fn bench_bubble_sort_with_benches_attribute(input: Vec<i32>) -> Vec<i32> {
    black_box(bubble_sort(input))
}

但是更简洁,尤其是当传递给相同的 setup 函数很多值时。

file 参数更进一步,它逐行读取指定的文件,并从每一行创建一个基准测试。行被传递给基准测试函数作为 String 或如果也提供了 setup 参数,则传递给 setup 函数。以下是一个假设你有一个名为 benches/inputs(相对路径解释为工作区根目录)的文件的小例子,内容如下

1
11
111

然后

use iai_callgrind::library_benchmark;
use std::hint::black_box;

#[library_benchmark]
#[benches::by_file(file = "benches/inputs")]
fn some_bench(line: String) -> Result<u64, String> {
    black_box(my_lib::string_to_u64(line))
}

/* ... */

上面的内容大致等同于以下内容,但是使用了 args 参数

use iai_callgrind::library_benchmark;
use std::hint::black_box;

#[library_benchmark]
#[benches::by_file(args = [1.to_string(), 11.to_string(), 111.to_string()])]
fn some_bench(line: String) -> Result<u64, String> {
    black_box(my_lib::string_to_u64(line))
}

/* ... */

从文件中读取输入允许例如在 criterion 等不同的基准测试框架之间共享相同的输入,或者如果你有一长串输入,你可能发现从文件中读取它们更加方便。

library_benchmark_group!

library_benchmark_group 宏接受以下参数(按顺序,并用分号分隔)

  • name(必填):用于在 main! 宏中识别组的唯一名称
  • config(可选):一个应用于同一组内所有基准测试的LibraryBenchmarkConfig
  • compare_by_id(可选):默认值为false。如果为true,则使用benchmarks参数指定的所有基准测试函数(跨任何基准测试组),只要id(在::之后的)匹配,就会相互比较。另请参阅下文
  • setup(可选):在运行该组内所有基准测试之前运行的一个设置函数或任何有效的表达式
  • teardown(可选):在运行该组内所有基准测试之后运行的一个清理函数或任何有效的表达式
  • benchmarks(必填):一个逗号分隔的基准测试函数路径列表,这些函数已使用#[library_benchmark]进行注解

请注意,setupteardown参数与#[library_benchmark]#[bench]#[benches]中的参数不同。它们接受表达式或函数调用,如setup = group_setup_function()。此外,这些setupteardown函数不会被上述任何属性覆盖。

比较基准测试函数

通过可选的library_benchmark_group!参数compare_by_idcompare_by_id的默认值为false)支持比较基准测试函数。只有具有相同id的基准测试才会比较,这允许突出显示不需要比较的案例。在以下示例中,除了与上次运行进行比较之外,还将case_3multiple基准测试相互比较。

use iai_callgrind::{library_benchmark, library_benchmark_group};
use std::hint::black_box;
use my_lib::bubble_sort;

#[library_benchmark]
#[bench::case_3(vec![1, 2, 3])]
#[benches::multiple(args = [vec![1, 2], vec![1, 2, 3, 4]])]
fn bench_bubble_sort_best_case(input: Vec<i32>) -> Vec<i32> {
    black_box(bubble_sort(input))
}

#[library_benchmark]
#[bench::case_3(vec![3, 2, 1])]
#[benches::multiple(args = [vec![2, 1], vec![4, 3, 2, 1]])]
fn bench_bubble_sort_worst_case(input: Vec<i32>) -> Vec<i32> {
    black_box(bubble_sort(input))
}

library_benchmark_group!(
    name = bench_bubble_sort;
    compare_by_id = true;
    benchmarks = bench_bubble_sort_best_case, bench_bubble_sort_worst_case
);

注意,如果compare_by_idtrue,则所有基准测试函数都将相互比较,因此您不需要在每个比较组中限制为两个基准测试函数。

以下是上述示例输出的精选摘录,以了解发生了什么

test_lib_bench_compare::bubble_sort_compare::bench_bubble_sort_best_case case_3:vec! [1, 2, 3]
  Instructions:                  94|N/A             (*********)
  L1 Hits:                      124|N/A             (*********)
  L2 Hits:                        0|N/A             (*********)
  RAM Hits:                       4|N/A             (*********)
  Total read+write:             128|N/A             (*********)
  Estimated Cycles:             264|N/A             (*********)
test_lib_bench_compare::bubble_sort_compare::bench_bubble_sort_worst_case case_3:vec! [3, 2, 1]
  Instructions:                 103|N/A             (*********)
  L1 Hits:                      138|N/A             (*********)
  L2 Hits:                        0|N/A             (*********)
  RAM Hits:                       5|N/A             (*********)
  Total read+write:             143|N/A             (*********)
  Estimated Cycles:             313|N/A             (*********)
  Comparison with bench_bubble_sort_best_case case_3:vec! [1, 2, 3]
  Instructions:                  94|103             (-8.73786%) [-1.09574x]
  L1 Hits:                      124|138             (-10.1449%) [-1.11290x]
  L2 Hits:                        0|0               (No change)
  RAM Hits:                       4|5               (-20.0000%) [-1.25000x]
  Total read+write:             128|143             (-10.4895%) [-1.11719x]
  Estimated Cycles:             264|313             (-15.6550%) [-1.18561x]

以下是比较算法的步骤

  1. 运行第一个基准测试函数中的所有基准测试
  2. 在第二个基准测试函数中运行第一个基准测试,如果第一个基准测试函数中存在具有相同id的基准测试,则比较它们
  3. 在第二个基准测试函数中运行第二个基准测试...
  4. ...
  5. 在第三个基准函数中运行第一个基准测试,如果第一个基准函数中存在具有相同ID的基准测试,则比较它们。如果第二个基准函数中存在具有相同ID的基准测试,则比较它们。
  6. 在第三个基准函数中运行第二个基准测试...
  7. 依次类推...直到所有基准测试都相互比较

基准函数内基准测试的顺序和数量并不重要,因此没有必要在第二个、第三个等基准函数中严格镜像第一个基准函数的基准测试ID。

配置

可以配置一些 iai-callgrind 的行为。有关 LibraryBenchmarkConfig 的详细信息,请参阅 docs。在顶级使用 main!

use iai_callgrind::{main, LibraryBenchmarkConfig};

main!(
    config = LibraryBenchmarkConfig::default();
    library_benchmark_groups = /* ... */
);

在组级别

use iai_callgrind::{library_benchmark_group, LibraryBenchmarkConfig};

library_benchmark_group!(
    name = some_name;
    config = LibraryBenchmarkConfig::default();
    benchmarks = /* ... */
);

library_benchmark 级别

use iai_callgrind::{library_benchmark, LibraryBenchmarkConfig};

#[library_benchmark(config = LibraryBenchmarkConfig::default())]
/* ... */

以及在 bench 级别

use iai_callgrind::{library_benchmark, LibraryBenchmarkConfig};

#[library_benchmark]
#[bench::some_id(args = (1, 2), config = LibraryBenchmarkConfig::default())]
/* ... */

bench 级别的配置会覆盖 library_benchmark 级别的配置。在 library_benchmark 级别的配置会覆盖组级别的配置,依此类推。请注意,如 envs 这样的配置值是累加的,并不会覆盖更高级别的配置值。

示例

有关一个完全文档化和可工作的基准测试,请参阅 test_lib_bench_groups 基准测试文件,并阅读 library documentation

二进制基准测试

使用此方案来基准测试您crate的一个或多个二进制文件或系统上安装的任何二进制文件。设置二进制基准测试的API几乎与库基准测试等效。本节重点关注差异。有关基础知识,请参阅 Library Benchmarks

重要默认行为

与库基准测试一样,在运行基准测试之前会清除基准测试二进制文件的环境变量。有关如何将环境变量传递给基准测试二进制文件的说明,请参阅 环境变量

快速入门

假设crate的二进制文件名为 my-foo,并且这个二进制文件接受一个文件路径作为位置参数。此第一个示例展示了使用带有 #[binary_benchmark] 属性的高级API的基本用法

use iai_callgrind::{binary_benchmark, binary_benchmark_group, main};

#[binary_benchmark]
#[bench::some_id("foo.txt")]
fn bench_binary(path: &str) -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .arg(path)
        .build()
}

binary_benchmark_group!(
    name = my_group;
    benchmarks = bench_binary
);

main!(binary_benchmark_groups = my_group);

或使用低级API几乎相同

use iai_callgrind::{BinaryBenchmark, Bench, binary_benchmark_group, main};

binary_benchmark_group!(
    name = my_group;
    benchmarks = |group: &mut BinaryBenchmarkGroup| {
        group.binary_benchmark(BinaryBenchmark::new("bench_binary")
            .bench(Bench::new("some_id")
                .command(iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
                    .arg("foo.txt")
                    .build()
                )
            )
        )
    }
);

main!(binary_benchmark_groups = my_group);

由于低级API已在 docs 中全面文档化,并且基本上反映了高级API,因此我们在此不深入探讨低级API的细节。

从库基准测试来看,包含 library 的名称更改为相同的名称,但将 library 替换为 binary,因此 #[library_benchmark] 属性的名称更改为 #[binary_benchmark]library_benchmark_group! 更改为 binary_benchmark_group!,配置参数接受一个 BinaryBenchmarkConfig 而不是 LibraryBenchmarkConfig...

最重要的区别在于,被注解的函数#[binary_benchmark]始终需要返回一个iai_callgrind::Command。注意,这个函数构建了将要进行基准测试的命令,但不会执行它。因此,这个函数中的代码不会对实际基准测试的事件计数产生影响。

use iai_callgrind::binary_benchmark;
use std::path::PathBuf;

#[binary_benchmark]
#[bench::foo("foo.txt")]
#[bench::bar("bar.json")]
fn bench_binary(path: &str) -> iai_callgrind::Command {
    // We can put any code in this function which is needed to configure and
    // build the `Command`.
    let path = PathBuf::from(path);

    // Here, if the `path` ends with `.txt` we want to see
    // the `Stdout` output of the `Command` in the benchmark output. In all other 
    // cases, the `Stdout` of the `Command` is redirected to a `File` with the
    // same name as the input `path` but with the extension `out`.
    let stdout = if path.extension() == "txt" {
        iai_callgrind::Stdio::Inherit
    } else {
        iai_callgrind::Stdio::File(path.with_extension("out"))
    };
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .stdout(stdout)
        .arg(path)
        .build()
}
// ... binary_benchmark_group! and main!
setupteardown

由于我们可以在函数本身中放置任何构建Command的代码,因此setupteardown#[binary_benchmark]#[bench]#[benches]中的工作方式不同。

use iai_callgrind::binary_benchmark;

fn create_file() {
    std::fs::write("foo.txt", "some content").unwrap();
}

#[binary_benchmark]
#[bench::foo("foo.txt", setup = create_file())]
fn bench_binary(path: &str) -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .arg(path)
        .build()
}

setup,这里是指表达式create_file(),并不会立即被评估,并且setup的返回值也不会作为输入传递给function!相反,setup中的表达式将在基准测试的Command执行前被评估和执行。同样,teardownCommand执行后被执行。在这个例子中,setup始终创建相同的文件,并且是相当静态的。可以使用相同的参数来使用路径或文件指针到函数的setupteardown)和函数。

use iai_callgrind::binary_benchmark;

fn create_file(path: &str) {
    std::fs::write(path, "some content").unwrap();
}

fn delete_file(path: &str) {
    std::fs::remove_file(path).unwrap();
}

#[binary_benchmark]
// Note the missing parentheses for `setup` of the function `create_file` which
// tells iai-callgrind to pass the `args` to the `setup` function AND the
// function `bench_binary`
#[bench::foo(args = ("foo.txt"), setup = create_file)]
// Same for `teardown`
#[bench::bar(args = ("bar.txt"), setup = create_file, teardown = delete_file)]
fn bench_binary(path: &str) -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .arg(path)
        .build()
}
// ... binary_benchmark_group! and main!
Sandbox

如上节所示,创建仅用于基准测试的文件的清理可能会变得很麻烦。出于这个目的以及许多其他原因,最好在一个临时目录中运行setupCommand本身和teardown。这个临时目录,即Sandbox,在基准测试结束后将被删除,无论基准测试是否成功。如果你只依赖于teardown,则后者并不保证,因为teardown仅在Command无错误返回时执行。

use iai_callgrind::{binary_benchmark, BinaryBenchmarkConfig, Sandbox};

fn create_file(path: &str) {
    std::fs::write(path, "some content").unwrap();
}

#[binary_benchmark]
#[bench::foo(
    args = ("foo.txt"),
    config = BinaryBenchmarkConfig::default().sandbox(Sandbox::new(true)),
    setup = create_file
)]
fn bench_binary(path: &str) -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .arg(path)
        .build()
}
// ... binary_benchmark_group! and main!

在这个例子中,作为setup的一部分,在Command执行之前,在Sandbox中执行了带有参数foo.txtcreate_file函数。在相同的Sandbox中执行Command,因此由于setup的存在,文件foo.txt及其内容some content存在。在执行Command和可能的teardown之后,将完全删除Sandbox,删除在setupCommand执行和teardown期间创建的所有文件。

由于setup在沙箱中运行,您现在不能再那么容易地从项目的 workspace 复制 fixtures 到沙箱中。可以通过在Sandbox::fixtures中使用Sandbox来配置将fixtures复制到临时目录。

use iai_callgrind::{binary_benchmark, BinaryBenchmarkConfig, Sandbox};

#[binary_benchmark]
#[bench::foo(
    args = ("foo.txt"),
    config = BinaryBenchmarkConfig::default()
        .sandbox(Sandbox::new(true)
            .fixtures(["benches/foo.txt"])),
)]
fn bench_binary(path: &str) -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .arg(path)
        .build()
}
// ... binary_benchmark_group! and main!

上述操作会将位于benches目录中的foo.txt这个 fixtures 文件复制到沙箱根目录,并以foo.txt的形式存在。在Sandbox::fixtures中的相对路径相对于 workspace 根目录进行解析。在一个多 crate 的 workspace 中,这是包含顶级Cargo.toml文件的目录。在Sandbox::fixtures中的路径不仅限于文件,也可以是目录。

如果您有更复杂的需求,可以在setupteardown中通过环境变量_WORKSPACE_ROOT访问 workspace 根目录。例如,假设有一个位于/home/the_project/foo_crate/benches/fixtures/foo.txt的 fixtures,其中the_project是 workspace 根目录,而foo_crate是一个包含my-foo可执行文件的 workspace 成员。如果命令期望创建一个bar.json文件,该文件在基准测试运行后需要进一步检查,我们可以将其复制到foo_crate中的临时目录tmp(可能存在或不存在)中。

use iai_callgrind::{binary_benchmark, BinaryBenchmarkConfig, Sandbox};
use std::path::PathBuf;

fn copy_fixture(path: &str) {
    let workspace_root = PathBuf::from(std::env::var_os("_WORKSPACE_ROOT").unwrap());
    std::fs::copy(
        workspace_root.join("foo_crate").join("benches").join("fixtures").join(path),
        path
    );
}

// This function will fail if `bar.json` does not exist, which is fine as this
// file is expected to be created by `my-foo`. So, if this file does not exist,
// an error will occur and the benchmark will fail. Although benchmarks are not
// expected to test the correctness of the application, the `teardown` can be
// used to check postconditions for a successful command run.
fn copy_back(path: &str) {
    let workspace_root = PathBuf::from(std::env::var_os("_WORKSPACE_ROOT").unwrap());
    let dest_dir = workspace_root.join("foo_crate").join("tmp");
    if !dest_dir.exists() {
        std::fs::create_dir(dest_dir).unwrap();
    }
    std::fs::copy(path, dest_dir.join(path));
}

#[binary_benchmark]
#[bench::foo(
    args = ("foo.txt"),
    config = BinaryBenchmarkConfig::default().sandbox(Sandbox::new(true)),
    setup = copy_fixture,
    teardown = copy_back("bar.json")
)]
fn bench_binary(path: &str) -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .arg(path)
        .build()
}

// ... binary_benchmark_group! and main!
命令的 stdin 和模拟管道输入

CommandStdin的行为可以改变,几乎与std::process::CommandStdin相同,唯一的区别是我们使用枚举iai_callgrind::Stdiniai_callgrind::Stdio。这些枚举提供了Inherit(与std::process::Stdio::inherit等价)、Pipe(与std::process::Stdio::piped等价)等变体。还有一个File,它接受一个指向文件的PathBuf,该文件被iai-callgrind重定向为CommandStdin

此外,iai_callgrind::Stdin还提供了针对iai-callgrind的特定变体Stdin::Setup

如果输入或CommandStdin来自管道,应用程序可能会改变其行为。为了能够对这些情况进行基准测试,可以将setup的输出用作CommandStdoutStderr的输入。

use iai_callgrind::{binary_benchmark, Stdin, Pipe};

fn setup_pipe() {
    println!(
        "The output to `Stdout` here will be the input or `Stdin` of the `Command`"
    );
}

#[binary_benchmark]
#[bench::foo(setup = setup_pipe())]
fn bench_binary() -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo"))
        .stdin(Stdin::Setup(Pipe::Stdout))
        .build()
}

// ... binary_benchmark_group! and main!

通常,先执行 setup,然后依次执行 Commandteardown,每个过程都会等待前一个过程成功退出(参见配置Command的退出码。如果 Command::stdin 变为 Stdin::Setup,则 setupCommand 并行执行,iai-callgrind 首先等待 Command 退出,然后等待 setup。在 setup 成功退出后,执行 teardown

配置Command的退出码

通常,如果一个 Command 以非零退出码退出,则整个基准测试运行失败并停止。如果期望基准测试的 Command 的退出码与 0 不同,可以在 BinaryBenchmarkConfig::exit_withCommand::exit_with 中设置期望的退出码。

use iai_callgrind::{binary_benchmark, BinaryBenchmarkConfig, ExitWith};

#[binary_benchmark]
// Here, we set the expected exit code of `my-foo` to 2
#[bench::foo(
    config = BinaryBenchmarkConfig::default().exit_with(ExitWith::Code(2))
)]
// Here, we don't know the exact exit code but know it is different from 0 (=success)
#[bench::bar(
    config = BinaryBenchmarkConfig::default().exit_with(ExitWith::Failure)
)]
fn bench_binary() -> iai_callgrind::Command {
    iai_callgrind::Command::new(env!("CARGO_BIN_EXE_my-foo")).build()
}

// ... binary_benchmark_group! and main!
示例

查看本项目的 test_bin_bench_intro 基准测试文件,以获取一个有效且完全文档化的示例。

性能回退

使用 Iai-Callgrind,您可以定义每个事件类型的性能回归限制。默认情况下没有回归检查,您需要通过在基准测试级别或全局级别使用 命令行参数或环境变量 来选择加入。

性能回归检查由一个 EventKind 和一个假设回归的百分比组成。如果百分比是负数,则假设回归低于此限制。默认的 EventKindEventKind::Ir,其值为 +10%。例如,在一个 库基准测试 中,让我们将默认限制更改为全局限制 +5%,适用于总指令数(Ir 事件类型)

use iai_callgrind::{main, LibraryBenchmarkConfig, RegressionConfig, EventKind};
/* library benchmarks and groups */
main!(
    config = LibraryBenchmarkConfig::default()
        .regression(
            RegressionConfig::default()
                .limits([(EventKind::Ir, 5.0)])
        );
    library_benchmark_groups = some_group
);

例如,SQLite 主要使用 CPU 指令来衡量性能改进(和回归)。

有关回归检查的更多详细信息,请参阅 iai-callgrind 的 docs

Valgrind 工具

除了默认的基准测试之外,您还可以使用 Iai-Callgrind 框架运行其他 Valgrind 分析 Tool,如 DHATMassif 和实验性的 BBV,以及如果需要检查基准测试代码的内存和线程安全性,还可以使用 MemcheckHelgrindDRD。有关更多详细信息以及命令行参数,请参阅 Valgrind 用户手册。额外的工具可以在 LibraryBenchmarkConfigBinaryBenchmarkConfig 中指定。例如,为所有库基准测试运行 DHAT

use iai_callgrind::{
    library_benchmark, library_benchmark_group, main, LibraryBenchmarkConfig, Tool,
    ValgrindTool
};

#[library_benchmark]
fn some_func() {
    println!("Hello, World!");
}

library_benchmark_group!(name = some_group; benchmarks = some_func);

main!(
    config = LibraryBenchmarkConfig::default()
                .tool(Tool::new(ValgrindTool::DHAT));
    library_benchmark_groups = some_group
);

所有产生 ERROR SUMMARY 的工具,如 (Memcheck, DRD, Helgrind),都设置了 --error-exitcode=201(参见此处),所以如果有错误,基准测试将失败并以 201 退出。您可以覆盖此默认值:

use iai_callgrind::{Tool, ValgrindTool};

Tool::new(ValgrindTool::Memcheck).args(["--error-exitcode=0"]);

这将恢复 valgrind 的默认值 0

Valgrind 客户端请求

iai-callgrind 随带其自己的接口,用于 Valgrind 的客户端请求机制。与在 C 代码中使用的 Valgrind 客户端请求相比,iai-callgrind's 客户端请求在许多 Valgrind 本地支持的靶机上具有零开销。我的观点可能存在偏见,但与其他提供 Valgrind 客户端请求接口的 crates 相比,iai-callgrind 提供了最完整和最佳性能的实现。

客户端请求默认是禁用的,但可以通过 client_requests 功能激活。

[dev-dependencies]
iai-callgrind = { version = "0.13.0", features = ["client_requests"] }

如果您需要在生产代码中使用客户端请求,通常不希望在 iai-callgrind 基准测试下运行时它们不执行任何操作。您可以通过将 iai-callgrindclient_requests_defs 功能添加到您的运行时依赖项,并将 client_requests 功能添加到您的 dev-dependencies 来实现这一点,如下所示:

[dependencies]
iai-callgrind = { version = "0.13.0", default-features = false, features = [
    "client_requests_defs"
] }

[dev-dependencies]
iai-callgrind = { version = "0.13.0", features = ["client_requests"] }

仅激活 client_requests_defs 功能时,客户端请求编译为空,不增加任何开销。它只是提供“定义”,方法签名和宏,但没有实现。只有激活了 client_requests 功能,它们才会实际执行。请注意,客户端请求不依赖于 iai-callgrind 的任何其他部分,因此您甚至可以在不使用 iai-callgrind 的其余部分的情况下使用客户端请求。

例如,在您的代码中使用它们如下所示:

use iai_callgrind::client_requests;

fn main() {
    // Start callgrind event counting if not already started earlier
    client_requests::callgrind::start_instrumentation();

    // do something important

    // Toggle callgrind event counting off
    client_requests::callgrind::toggle_collect();
}

当使用客户端请求构建 iai-callgrind 时,Valgrind 的头文件必须在您的标准包含路径中存在(通常是 /usr/include)。如果您使用发行版的包管理器安装了 Valgrind,通常就是这样。如果不是,您可以设置环境变量 IAI_CALLGRIND_VALGRIND_INCLUDEIAI_CALLGRIND_<triple>_VALGRIND_INCLUDE 指向包含路径。例如,如果头文件可以在 /home/foo/repo/valgrind/{valgrind.h, callgrind.h, ...} 中找到,则正确的包含路径将是 IAI_CALLGRIND_VALGRIND_INCLUDE=/home/foo/repo(不是 /home/foo/repo/valgrind)。

这只是一个小介绍,请参阅docs以获取更多详细信息!

火焰图

火焰图是可选的,如果您将 FlamegraphConfig 传递给 BinaryBenchmarkConfigLibraryBenchmarkConfig,就可以创建火焰图。Callgrind 火焰图是 valgrind 的可视化工具 callgrind_annotatekcachegrind 的补充。

Callgrind 火焰图显示了函数和单个 EventKind(默认为 EventKind::Ir)的包含成本,类似于 callgrind_annotate,但以更美观(且可点击)的方式呈现。特别是,差异火焰图有助于更深入地理解导致瓶颈或性能退化的代码部分等。

生成的火焰图 *.svg 文件位于 target/iai 目录中,与相应的 callgrind 输出文件相邻。

命令行参数和环境变量

可以通过 --cargo bench -- ARGS)将参数传递给 iai-callgrind。如果您遇到错误 Unrecognized Option,请参阅 常见问题解答。要查看所有可能的参数,请执行 cargo bench --bench <benchmark> -- --help。几乎所有的命令行参数都有一个相应的环境变量。没有相应命令行参数的环境变量包括

  • IAI_CALLGRIND_COLOR:控制 iai-callgrind 的彩色输出。(默认为 auto
  • IAI_CALLGRIND_LOG:定义日志级别(默认为 WARN

与基准比较

通常,连续两次基准运行可以让 iai-callgrind 比较这两次运行。有时,您可能希望将当前的基准运行与一个静态参考进行比较。例如,如果您在某个功能的实现上花费了更多时间,您可能希望将其与另一个分支的基准或您开始修改新功能的提交进行比较,以确保您没有引入性能退化。iai-callgrind 提供了这样的自定义基准。如果您熟悉 criterion.rs,以下命令行参数也应该非常熟悉

  • --save-baseline=NAME:如果有,则与 NAME 基准进行比较,然后覆盖它。(环境变量:IAI_CALLGRIND_SAVE_BASELINE
  • --baseline=NAME:不覆盖它,与 NAME 基准进行比较(环境变量:IAI_CALLGRIND_BASELINE
  • --load-baseline=NAME:将 NAME 基准作为新的数据集加载,而不是创建一个新的基准。此选项还需要存在 --baseline=NAME。 (环境变量:IAI_CALLGRIND_LOAD_BASELINE

如果 NAME 不存在,则默认为 default

例如,要从主分支创建静态参考并比较它

git checkout main
cargo bench --bench <benchmark> -- --save-baseline=main
git checkout feature
# ... HACK ... HACK
cargo bench --bench <benchmark> -- --baseline main

自定义输出目录

默认情况下,所有基准测试输出文件都存储在 $PROJECT_ROOT/target/iai 目录树中。可以通过 IAI_CALLGRIND_HOME 环境变量或命令行参数 --home 来更改主目录。命令行参数会覆盖环境变量的值。例如,要将所有文件存储在 /tmp/iai-callgrind 目录下,可以使用 IAI_CALLGRIND_HOME=/tmp/iai-callgrindcargo bench -- --home=/tmp/iai-callgrind

如果您在不同的目标上运行基准测试,需要将每个目标的基准测试输出文件分开,否则可能会导致将基准测试与错误的目标进行比较,从而得到奇怪的结果。您可以通过每个目标的基线来实现这一点,但使用 --separate-targets 命令行参数或设置环境变量 IAI_CALLGRIND_SEPARATE_TARGETS=yes 来按目标分开输出文件会更容易。输出目录结构从 target/iai/$PACKAGE_NAME/$BENCHMARK_FILE/$GROUP/$BENCH_FUNCTION.$BENCH_ID 更改为 target/iai/$TARGET_TRIPLE/$PACKAGE_NAME/$BENCHMARK_FILE/$GROUP/$BENCH_FUNCTION.$BENCH_ID

例如,以下库基准测试的输出目录,假设基准测试文件名为 bench_file,在包 my_package

use iai_callgrind::{main, library_benchmark_group, library_benchmark};
use my_lib::some_function;

#[library_benchmark]
#[bench::short(10)]
fn bench_function(value: u64) -> u64 {
    some_function(value)
}

library_benchmark_group!(
    name = bench_group;
    benchmarks = bench_function
);

main!(library_benchmark_groups = bench_group);

未使用 --separate-targets

target/iai/my_package/bench_file/bench_group/bench_function.short

以及使用 --separate-targets,假设您正在运行 x86_64-unknown-linux-gnu 目标的基准测试

target/iai/x86_64-unknown-linux-gnu/my_package/bench_file/bench_group/bench_function.short

机器可读输出

使用 --output-format=default|json|pretty-json(环境变量:IAI_CALLGRIND_OUTPUT_FORMAT)可以将终端输出格式更改为机器可读的 json 格式。完全描述 json 输出的 json 模式存储在 summary.v2.schema.json。每行 json 输出(如果不是 pretty-json)是单个基准测试的摘要,您可能希望将所有基准测试组合到一个数组中。例如,可以使用 jq 来实现这一点:

cargobench ----output-format=json| jq -s

它将 {...}\n{...} 转换为 [{...},{...}]

除了或与更改终端输出相结合,还可以使用--save-summary=json|pretty-json(环境变量:IAI_CALLGRIND_SAVE_SUMMARY)为每个基准保存一个摘要文件。摘要文件summary.json存储在target/iai目录中,与通常的基准输出文件相邻。

其他输出选项

本节描述了其他影响iai-callgrind终端和日志输出的命令行选项和环境变量。

显示基准的终端输出

默认情况下,所有终端输出都会被捕获,因此在基准运行期间不会显示。要显示任何捕获的输出,可以使用IAI_CALLGRIND_LOG=info。另一种可能是,使用--nocapture(环境变量:IAI_CALLGRIND_NOCAPTURE)选项告诉iai-callgrind不要捕获输出。这目前仅限于callgrind运行,以防止多次显示相同的输出。因此,其他工具运行(请参阅Valgrind Tools)的任何终端输出仍然会被捕获。

--nocapture标志除了接受truefalse之外,还接受特殊的值stdoutstderr。在--nocapture=stdout的情况下,终端输出到stdout不会被捕获并在基准运行期间显示,但输出到stderr将被丢弃。同样,--nocapture=stderr显示终端输出到stderr,但丢弃输出到stdout

例如,一个库基准benches/my_benchmark.rs

use iai_callgrind::{main, library_benchmark_group, library_benchmark};

fn my_teardown(value: u64) {
    eprintln!("Error output during teardown: {value}");
}

fn to_be_benchmarked(value: u64) -> u64 {
    println!("Output to stdout: {value}");
    value + 10
}

#[library_benchmark]
#[bench::some_id(args = (10), teardown = my_teardown)]
fn my_bench(value: u64) -> u64 {
    to_be_benchmarked(value)
}

library_benchmark_group!(
    name = my_bench_group;
    benchmarks = my_bench
);

main!(library_benchmark_groups = my_bench_group);

如果以上基准使用cargo bench --bench my_benchmark -- --nocapture运行,iai-callgrind的输出将如下(这里指令和其他值不重要,是虚构的)

my_benchmark::my_bench_group::my_bench some_id:10
Output to stdout: 10
Error output during teardown: 20
- end of stdout/stderr
  Instructions:              331082|N/A             (*********)
  L1 Hits:                   442452|N/A             (*********)
  L2 Hits:                      720|N/A             (*********)
  RAM Hits:                    3926|N/A             (*********)
  Total read+write:          447098|N/A             (*********)
  Estimated Cycles:          583462|N/A             (*********)

请注意,无论--nocapture选项的值如何,valgrind工具本身的全部日志输出都会存储在基准输出目录中的文件中。由于iai-callgrind需要存储在文件中的valgrind工具的日志输出,因此没有选项可以禁用这些日志文件的创建。但如果出现问题,你可能很高兴有日志文件。

更改颜色输出

默认情况下,终端输出是彩色的,但遵循环境变量IAI_CALLGRIND_COLOR的值。如果未设置IAI_CALLGRIND_COLOR,则会尝试设置CARGO_TERM_COLOR。接受的值是:alwaysneverauto(默认)。因此,禁用颜色可以通过设置IAI_CALLGRIND_COLORCARGO_TERM_COLOR=never

更改日志输出

本库使用了env_logger和默认的日志级别WARN。要设置不同的日志级别,请设置环境变量IAI_CALLGRIND_LOG,例如将其设置为IAI_CALLGRIND_LOG=DEBUG。接受的值有:errorwarn(默认)、infodebugtrace。默认情况下,日志输出是带颜色的,但会遵循IAI_CALLGRIND_COLORCARGO_TERM_COLOR的设置(优先级顺序)。有关更多信息,请参阅env_logger文档

iai 的特性和差异

此crate类似于原始的Iai,但经过时间积累,进行了很多改进。最大的不同之处在于,它底层使用Callgrind而不是Cachegrind。

更稳定的指标

Iai-Callgrind在跨不同系统时具有更精确和稳定的指标。它是通过以下方式实现的:

  • 只计算基准测试函数内函数调用的事件。这种行为在虚拟上封装了基准测试函数,并将基准测试与周围代码分开。
  • 将iai库与主宏与实际的运行器分开。这也是为什么需要额外安装iai-callgrind-runner的原因,但在这种分离之前,即使iai库中的微小更改也会影响正在测试的基准。

以下是一个基准的本地运行示例

$ cd iai-callgrind
$ cargo bench --bench test_lib_bench_readme_example_fibonacci
test_lib_bench_readme_example_fibonacci::bench_fibonacci_group::bench_fibonacci short:10
  Instructions:                1733|N/A             (*********)
  L1 Hits:                     2359|N/A             (*********)
  L2 Hits:                        0|N/A             (*********)
  RAM Hits:                       2|N/A             (*********)
  Total read+write:            2361|N/A             (*********)
  Estimated Cycles:            2429|N/A             (*********)
test_lib_bench_readme_example_fibonacci::bench_fibonacci_group::bench_fibonacci long:30
  Instructions:            26214733|N/A             (*********)
  L1 Hits:                 35638617|N/A             (*********)
  L2 Hits:                        0|N/A             (*********)
  RAM Hits:                       4|N/A             (*********)
  Total read+write:        35638621|N/A             (*********)
  Estimated Cycles:        35638757|N/A             (*********)

为了比较,相同基准在github CI中的输出产生相同的结果。通常,CI运行和本地运行之间几乎没有任何差异,这使得基准测试的运行和基准代码的性能改进在系统间更具可比性。

Valgrind注释工具的输出更清晰

现在已废弃的Iai校准运行只是固定了Iai自身的摘要输出,但cg_annotate的输出仍然被设置函数和指标所杂乱。Iai-Callgrind产生的callgrind_annotate输出要干净得多,并且集中在实际测试的函数上。

重新设计指标输出

虽然仍然相关,但基准测试的统计数据大多不再与原始Iai兼容。现在还包括一些额外的信息

test_lib_bench_readme_example_fibonacci::bench_fibonacci_group::bench_fibonacci short:10
  Instructions:                1733|N/A             (*********)
  L1 Hits:                     2359|N/A             (*********)
  L2 Hits:                        0|N/A             (*********)
  RAM Hits:                       2|N/A             (*********)
  Total read+write:            2361|N/A             (*********)
  Estimated Cycles:            2429|N/A             (*********)

有一行额外的Total read+write,它总结了上面带有Hits的行以及改为L1 HitsL1 Accesses行。

详细说明

总读取+写入= L1命中数+ L2命中数+ RAM命中数.

估计周期公式没有变化,并使用Itamar Turner-Trauring的公式,来自https://pythonspeed.com/articles/consistent-benchmarking-in-ci/

估计周期= L1命中数+ 5×(L2命中数) + 35×(RAM命中数)

有关缓存模拟的更多信息,请参阅Callgrind的文档

未更改的内容

Iai-Callgrind无法完全消除设置变化的影响。但是,这些影响不应该再显著。

常见问题解答

我得到错误Sentinel ... not found

您可能已禁用了在 cargo bench 配置文件中创建调试符号。这可能是由于您在 release 配置文件中添加了选项,而 bench 配置文件继承了 release 配置文件。例如,如果您已将 strip = true 添加到 release 配置文件中,这是完全可以的,但您需要在 bench 配置文件中禁用此选项才能运行 iai-callgrind 基准测试。有关更详细的示例,请参阅基准测试部分。

运行 cargo bench 会导致“未识别的选项”错误

为了使 cargo bench -- --some-valid-arg 工作正常,您可以指定基准测试使用 --bench BENCHMARK,例如 cargo bench --bench my_iai_benchmark -- --callgrind-args="--collect-bus=yes",或者将以下内容添加到您的 Cargo.toml

[lib]
bench = false

并且如果您有二进制文件

[[bin]]
name = "my-binary"
path = "src/bin/my-binary.rs"
bench = false

bench = false 设置为禁用隐式创建的默认 libtest 测试框架,即使您没有在库或二进制文件中使用 #[bench] 函数,也会添加此框架。当然,默认框架不了解 iai-callgrind 参数,并在打印 未识别的 选项 错误时终止执行。

如果您无法或不想将 bench = false 添加到您的 Cargo.toml,您可以使用环境变量作为替代。对于每个 命令行参数 都有一个相应的环境变量。

贡献

关于为 iai-callgrind 做贡献的指南可以在CONTRIBUTING.md 文件中找到。

除非您明确表示,否则您有意提交给工作的任何贡献都将按照许可证进行双重许可,不附加任何额外条款或条件。

另请参阅

致谢

Iai-Callgrind 是从 https://github.com/bheisler/iai 分支出来的,最初由 Brook Heisler (@bheisler) 编写。

Iai-Callgrind 的实现离不开 Valgrind

许可证

Iai-Callgrind 类似 Iai,可以选择Apache 2.0许可证或MIT许可证。

根据Valgrind文档

Valgrind头文件与大多数其他代码不同,采用BSD风格许可证,因此您可以包含它们而不用担心许可证不兼容问题。

我们在使用原始头文件的地方包含了原始许可证。

依赖项

~0–510KB
~11K SLoC