Attribute Macro divan::bench

source ·
#[bench]
Expand description

Registers a benchmarking function.

§Examples

The quickest way to get started is to benchmark the function as-is:

use divan::black_box;

#[divan::bench]
fn add() -> i32 {
    black_box(1) + black_box(42)
}

fn main() {
    // Run `add` benchmark:
    divan::main();
}

If benchmarks need to setup context before running, they can take a Bencher and use Bencher::bench:

use divan::{Bencher, black_box};

#[divan::bench]
fn copy_from_slice(bencher: Bencher) {
    let src = (0..100).collect::<Vec<i32>>();
    let mut dst = vec![0; src.len()];

    bencher.bench_local(move || {
        black_box(&mut dst).copy_from_slice(black_box(&src));
    });
}

Applying this attribute multiple times to the same item will cause a compile error:

#[divan::bench]
#[divan::bench]
fn bench() {
    // ...
}

§Drop

When a benchmarked function returns a value, it will not be dropped until after the current sample loop is finished. This allows for more precise timing measurements.

Note that there is an inherent memory cost to defer drop, including allocations inside not-yet-dropped values. Also, if the benchmark panics, the values will never be dropped.

The following example benchmarks will only measure String construction time, but not deallocation time:

use divan::{Bencher, black_box};

#[divan::bench]
fn freestanding() -> String {
    black_box("hello").to_uppercase()
}

#[divan::bench]
fn contextual(bencher: Bencher) {
    // Setup:
    let s: String = // ...

    bencher.bench(|| -> String {
        black_box(&s).to_lowercase()
    });
}

If the returned value does not need to be dropped, there is no memory cost. Because of this, the following example benchmarks are equivalent:

#[divan::bench]
fn with_return() -> i32 {
    let n: i32 = // ...
    n
}

#[divan::bench]
fn without_return() {
    let n: i32 = // ...
    divan::black_box(n);
}

§Options

§name

By default, the benchmark uses the function’s name. It can be overridden via the name option:

#[divan::bench(name = "my_add")]
fn add() -> i32 {
    // Will appear as "crate_name::my_add".
}

§crate

The path to the specific divan crate instance used by this macro’s generated code can be specified via the crate option. This is applicable when using divan via a macro from your own crate.

extern crate divan as sofa;

#[::sofa::bench(crate = ::sofa)]
fn add() -> i32 {
    // ...
}

§args

Function arguments can be provided to benchmark the function over multiple cases. This is used for comparing across parameters like collection lengths and enum variants. If you are not comparing cases and just need to pass a value into the benchmark, instead consider passing local values into the Bencher::bench closure or use Bencher::with_inputs for many distinct values.

The following example benchmarks converting a Range to Vec over different lengths:

#[divan::bench(args = [1000, LEN, len()])]
fn init_vec(len: usize) -> Vec<usize> {
    (0..len).collect()
}

const LEN: usize = // ...

fn len() -> usize {
    // ...
}

The list of arguments can be shared across multiple benchmarks through an external Iterator:

const LENS: &[usize] = // ...

#[divan::bench(args = LENS)]
fn bench_vec1(len: usize) -> Vec<usize> {
    // ...
}

#[divan::bench(args = LENS)]
fn bench_vec2(len: usize) -> Vec<usize> {
    // ...
}

Unlike the consts option, any argument type is supported if it implements Any, Copy, Send, Sync, and ToString (or Debug):

#[derive(Clone, Copy, Debug)]
enum Arg {
    A, B
}

#[divan::bench(args = [Arg::A, Arg::B])]
fn bench_args(arg: Arg) {
    // ...
}

The argument type does not need to implement Copy if it is used through a reference:

#[derive(Debug)]
enum Arg {
    A, B
}

#[divan::bench(args = [Arg::A, Arg::B])]
fn bench_args(arg: &Arg) {
    // ...
}

For convenience, common string types are coerced to &str:

fn strings() -> impl Iterator<Item = String> {
    // ...
}

#[divan::bench(args = strings())]
fn bench_strings(s: &str) {
    // ...
}

Arguments can also be used with Bencher. This allows for generating inputs based on args values or providing throughput information via Counters:

use divan::Bencher;

#[divan::bench(args = [1, 2, 3])]
fn bench(bencher: Bencher, len: usize) {
    let value = new_value(len);

    bencher
        .counter(len)
        .bench(|| {
            do_work(value);
        });
}

§consts

Divan supports benchmarking functions with const generics via the consts option.

The following example benchmarks initialization of [i32; N] for values of N provided by a literal, const item, and const fn:

#[divan::bench(consts = [1000, LEN, len()])]
fn init_array<const N: usize>() -> [i32; N] {
    let mut result = [0; N];

    for i in 0..N {
        result[i] = divan::black_box(i as i32);
    }

    result
}

const LEN: usize = // ...

const fn len() -> usize {
    // ...
}

The list of constants can be shared across multiple benchmarks through an external array or slice:

const SIZES: &[usize] = &[1, 2, 5, 10];

#[divan::bench(consts = SIZES)]
fn bench_array1<const N: usize>() -> [i32; N] {
    // ...
}

#[divan::bench(consts = SIZES)]
fn bench_array2<const N: usize>() -> [i32; N] {
    // ...
}

External constants are limited to lengths 1 through 20, because of implementation details. This limit does not apply if the list is provided directly like in the first example.

const SIZES: [usize; 21] = [
    // ...
];

#[divan::bench(consts = SIZES)]
fn bench_array<const N: usize>() -> [i32; N] {
    // ...
}

§types

Divan supports benchmarking generic functions over a list of types via the types option.

The following example benchmarks the From<&str> implementations for &str and String:

#[divan::bench(types = [&str, String])]
fn from_str<'a, T>() -> T
where
    T: From<&'a str>,
{
    divan::black_box("hello world").into()
}

The types and args options can be combined to benchmark T × A scenarios. The following example benchmarks the FromIterator implementations for Vec, BTreeSet, and HashSet:

use std::collections::{BTreeSet, HashSet};

#[divan::bench(
    types = [Vec<i32>, BTreeSet<i32>, HashSet<i32>],
    args = [0, 2, 4, 16, 256, 4096],
)]
fn from_range<T>(n: i32) -> T
where
    T: FromIterator<i32>,
{
    (0..n).collect()
}

§sample_count

The number of statistical sample recordings can be set to a predetermined u32 value via the sample_count option. This may be overridden at runtime using either the DIVAN_SAMPLE_COUNT environment variable or --sample-count CLI argument.

#[divan::bench(sample_count = 1000)]
fn add() -> i32 {
    // ...
}

If the threads option is enabled, sample count becomes a multiple of the number of threads. This is because each thread operates over the same sample size to ensure there are always N competing threads doing the same amount of work.

§sample_size

The number iterations within each statistics sample can be set to a predetermined u32 value via the sample_size option. This may be overridden at runtime using either the DIVAN_SAMPLE_SIZE environment variable or --sample-size CLI argument.

#[divan::bench(sample_size = 1000)]
fn add() -> i32 {
    // ...
}

§threads

Benchmarked functions can be run across multiple threads via the threads option. This enables you to measure contention on atomics and locks. The default thread count is the available parallelism.

use std::sync::Arc;

#[divan::bench(threads)]
fn arc_clone(bencher: divan::Bencher) {
    let arc = Arc::new(42);

    bencher.bench(|| arc.clone());
}

The threads option can be set to any of:

#[divan::bench(threads = false)]
fn single() {
    // ...
}

#[divan::bench(threads = 10)]
fn specific() {
    // ...
}

#[divan::bench(threads = 0..=8)]
fn range() {
    // Note: Includes 0 for available parallelism.
}

#[divan::bench(threads = [0, 1, 4, 8, 16])]
fn selection() {
    // ...
}

§counters

The Counters of each iteration can be set via the counters option. The following example emits info for the number of bytes and number of ints processed when benchmarking slice sorting:

use divan::{Bencher, counter::{BytesCount, ItemsCount}};

const INTS: &[i32] = &[
    // ...
];

#[divan::bench(counters = [
    BytesCount::of_slice(INTS),
    ItemsCount::new(INTS.len()),
])]
fn sort(bencher: Bencher) {
    bencher
        .with_inputs(|| INTS.to_vec())
        .bench_refs(|ints| ints.sort());
}

For convenience, singular counter allows a single Counter to be set. The following example emits info for the number of bytes processed when benchmarking char-counting:

use divan::counter::BytesCount;

const STR: &str = "...";

#[divan::bench(counter = BytesCount::of_str(STR))]
fn char_count() -> usize {
    divan::black_box(STR).chars().count()
}

See:

§bytes_count

Convenience shorthand for counter = BytesCount::from(n).

§chars_count

Convenience shorthand for counter = CharsCount::from(n).

§items_count

Convenience shorthand for counter = ItemsCount::from(n).

§min_time

The minimum time spent benchmarking each function can be set to a predetermined Duration via the min_time option. This may be overridden at runtime using either the DIVAN_MIN_TIME environment variable or --min-time CLI argument.

Unless skip_ext_time is set, this includes time external to the benchmarked function, such as time spent generating inputs and running Drop.

use std::time::Duration;

#[divan::bench(min_time = Duration::from_secs(3))]
fn add() -> i32 {
    // ...
}

For convenience, min_time can also be set with seconds as u64 or f64. Invalid values will cause a panic at runtime.

#[divan::bench(min_time = 2)]
fn int_secs() -> i32 {
    // ...
}

#[divan::bench(min_time = 1.5)]
fn float_secs() -> i32 {
    // ...
}

§max_time

The maximum time spent benchmarking each function can be set to a predetermined Duration via the max_time option. This may be overridden at runtime using either the DIVAN_MAX_TIME environment variable or --max-time CLI argument.

Unless skip_ext_time is set, this includes time external to the benchmarked function, such as time spent generating inputs and running Drop.

If min_time > max_time, then max_time has priority and min_time will not be reached.

use std::time::Duration;

#[divan::bench(max_time = Duration::from_secs(5))]
fn add() -> i32 {
    // ...
}

For convenience, like min_time, max_time can also be set with seconds as u64 or f64. Invalid values will cause a panic at runtime.

#[divan::bench(max_time = 8)]
fn int_secs() -> i32 {
    // ...
}

#[divan::bench(max_time = 9.5)]
fn float_secs() -> i32 {
    // ...
}

§skip_ext_time

By default, min_time and max_time include time external to the benchmarked function, such as time spent generating inputs and running Drop. Enabling the skip_ext_time option will instead make those options only consider time spent within the benchmarked function. This may be overridden at runtime using either the DIVAN_SKIP_EXT_TIME environment variable or --skip-ext-time CLI argument.

In the following example, max_time only considers time spent running measured_function:

#[divan::bench(max_time = 5, skip_ext_time)]
fn bench(bencher: divan::Bencher) {
    bencher
        .with_inputs(|| generate_input())
        .bench_values(|input| measured_function(input));
}

This option can be set to an explicit bool value to override parent values:

#[divan::bench(max_time = 5, skip_ext_time = false)]
fn bench(bencher: divan::Bencher) {
    // ...
}

§ignore

Like #[test], #[divan::bench] functions can use #[ignore]:

#[divan::bench]
#[ignore]
fn todo() {
    unimplemented!();
}

This option can also instead be set within the #[divan::bench] attribute:

#[divan::bench(ignore)]
fn todo() {
    unimplemented!();
}

Like skip_ext_time, this option can be set to an explicit bool value to override parent values:

#[divan::bench(ignore = false)]
fn bench() {
    // ...
}

This can be used to ignore benchmarks based on a runtime condition. The following example benchmark will be ignored if an environment variable is not set to “true”:

#[divan::bench(
    ignore = std::env::var("BENCH_EXPENSIVE").as_deref() != Ok("true")
)]
fn expensive_bench() {
    // ...
}