setup and teardown
setup
and teardown
are your bread and butter in library benchmarks. The
benchmark functions need to be as clean as possible and almost always only
contain the function call to the function of your library which you want to
benchmark.
Setup
In an ideal world you don't need any setup code, and you can pass arguments to the function as they are.
But, for example if a function expects a File
and not a &str
with the path
to the file you need setup
code. Iai-Callgrind has an easy-to-use system in
place to allow you to run any setup code before the function is executed and
this setup
code is not attributed to the metrics of the benchmark.
If the setup
parameter is specified, the setup
function takes the arguments
from the #[bench]
(or #[benches]
) attributes and the benchmark function
receives the return value of the setup
function as parameter. This is a small
indirection with great effect. The effect is best shown with an example:
extern crate iai_callgrind; mod my_lib { pub fn count_bytes_fast(_file: std::fs::File) -> u64 { 1 } } use iai_callgrind::{library_benchmark, library_benchmark_group, main}; use std::hint::black_box; use std::path::PathBuf; use std::fs::File; fn open_file(path: &str) -> File { File::open(path).unwrap() } #[library_benchmark] #[bench::first(args = ("path/to/file"), setup = open_file)] fn count_bytes_fast(file: File) -> u64 { black_box(my_lib::count_bytes_fast(file)) } library_benchmark_group!(name = my_group; benchmarks = count_bytes_fast); fn main() { main!(library_benchmark_groups = my_group); }
You can actually see the effect of using a setup function in the output of the
benchmark. Let's assume the above benchmark is in a file
benches/my_benchmark.rs
, then running
IAI_CALLGRIND_NOCAPTURE=true cargo bench
result in the benchmark output like below.
my_benchmark::my_group::count_bytes_fast first:open_file("path/to/file")
Instructions: 1630162|N/A (*********)
L1 Hits: 2507933|N/A (*********)
L2 Hits: 2|N/A (*********)
RAM Hits: 11|N/A (*********)
Total read+write: 2507946|N/A (*********)
Estimated Cycles: 2508328|N/A (*********)
The description in the headline contains open_file("path/to/file")
, your setup
function open_file
with the value of the parameter it is called with.
If you need to specify the same setup
function for all (or almost all)
#[bench]
and #[benches]
in a #[library_benchmark]
you can use the setup
parameter of the #[library_benchmark]
:
extern crate iai_callgrind; mod my_lib { pub fn count_bytes_fast(_file: std::fs::File) -> u64 { 1 } } use iai_callgrind::{library_benchmark, library_benchmark_group, main}; use std::hint::black_box; use std::path::PathBuf; use std::fs::File; use std::io::{Seek, SeekFrom}; fn open_file(path: &str) -> File { File::open(path).unwrap() } fn open_file_with_offset(path: &str, offset: u64) -> File { let mut file = File::open(path).unwrap(); file.seek(SeekFrom::Start(offset)).unwrap(); file } #[library_benchmark(setup = open_file)] #[bench::small("path/to/small")] #[bench::big("path/to/big")] #[bench::with_offset(args = ("path/to/big", 100), setup = open_file_with_offset)] fn count_bytes_fast(file: File) -> u64 { black_box(my_lib::count_bytes_fast(file)) } library_benchmark_group!(name = my_group; benchmarks = count_bytes_fast); fn main() { main!(library_benchmark_groups = my_group); }
The above will use the open_file
function in the small
and big
benchmarks
and the open_file_with_offset
function in the with_offset
benchmark.
Teardown
What about teardown
and why should you use it? Usually the teardown
isn't
needed but for example if you intend to make the result from the benchmark
visible in the benchmark output, the teardown
is the perfect place to do so.
The teardown
function takes the return value of the benchmark function as its
argument:
extern crate iai_callgrind; mod my_lib { pub fn count_bytes_fast(_file: std::fs::File) -> u64 { 1 } } use iai_callgrind::{library_benchmark, library_benchmark_group, main}; use std::hint::black_box; use std::path::PathBuf; use std::fs::File; fn open_file(path: &str) -> File { File::open(path).unwrap() } fn print_bytes_read(num_bytes: u64) { println!("bytes read: {num_bytes}"); } #[library_benchmark] #[bench::first( args = ("path/to/big"), setup = open_file, teardown = print_bytes_read )] fn count_bytes_fast(file: File) -> u64 { black_box(my_lib::count_bytes_fast(file)) } library_benchmark_group!(name = my_group; benchmarks = count_bytes_fast); fn main() { main!(library_benchmark_groups = my_group); }
Note Iai-Callgrind captures all output per default. In order to actually see the
output of the benchmark, setup
and teardown
functions, it is required to run
the benchmarks with the flag --nocapture
or set the environment variable
IAI_CALLGRIND_NOCAPTURE=true
. Let's assume the above benchmark is in a file
benches/my_benchmark.rs
, then running
IAI_CALLGRIND_NOCAPTURE=true cargo bench
results in output like the below
my_benchmark::my_group::count_bytes_fast first:open_file("path/to/big")
bytes read: 25078
- end of stdout/stderr
Instructions: 1630162|N/A (*********)
L1 Hits: 2507931|N/A (*********)
L2 Hits: 2|N/A (*********)
RAM Hits: 13|N/A (*********)
Total read+write: 2507946|N/A (*********)
Estimated Cycles: 2508396|N/A (*********)
The output of the teardown
function is now visible in the benchmark output
above the - end of stdout/stderr
line.