[go: up one dir, main page]

Function black_box

1.66.0 (const: 1.86.0) · Source
pub const fn black_box<T>(dummy: T) -> T
Expand description

Prevents compiler optimizations on a value.

black_box should only be used on inputs and outputs of benchmarks. Newcomers to benchmarking may be tempted to also use black_box within the implementation, but doing so will overly pessimize the measured code without any benefit.

§Benchmark Inputs

When benchmarking, it’s good practice to ensure measurements are accurate by preventing the compiler from optimizing based on assumptions about benchmark inputs.

The compiler can optimize code for indices it knows about, such as by removing bounds checks or unrolling loops. If real-world use of your code would not know indices up front, consider preventing optimizations on them in benchmarks:

use divan::black_box;

const INDEX: usize = // ...
const SLICE: &[u8] = // ...

#[divan::bench]
fn bench() {
    work(&SLICE[black_box(INDEX)..]);
}

The compiler may also optimize for the data itself, which can also be avoided with black_box:

#[divan::bench]
fn bench() {
    work(black_box(&SLICE[black_box(INDEX)..]));
}

§Benchmark Outputs

When benchmarking, it’s best to ensure that all of the code is actually being run. If the compiler knows an output is unused, it may remove the code that generated the output. This optimization can make benchmarks appear much faster than they really are.

At the end of a benchmark, we can force the compiler to treat outputs as if they were actually used:

#[divan::bench]
fn bench() {
    black_box(value.to_string());
}

To make the code clearer to readers that the output is discarded, this code could instead call black_box_drop.

Alternatively, the output can be returned from the benchmark:

#[divan::bench]
fn bench() -> String {
    value.to_string()
}

Returning the output will black_box it and also avoid measuring the time to drop the output, which in this case is the time to deallocate a String. Read more about this in the #[divan::bench] docs.


Standard Library Documentation

An identity function that hints to the compiler to be maximally pessimistic about what black_box could do.

Unlike std::convert::identity, a Rust compiler is encouraged to assume that black_box can use dummy in any possible valid way that Rust code is allowed to without introducing undefined behavior in the calling code. This property makes black_box useful for writing code in which certain optimizations are not desired, such as benchmarks.

Note however, that black_box is only (and can only be) provided on a “best-effort” basis. The extent to which it can block optimisations may vary depending upon the platform and code-gen backend used. Programs cannot rely on black_box for correctness, beyond it behaving as the identity function. As such, it must not be relied upon to control critical program behavior. This also means that this function does not offer any guarantees for cryptographic or security purposes.

§When is this useful?

While not suitable in those mission-critical cases, black_box’s functionality can generally be relied upon for benchmarking, and should be used there. It will try to ensure that the compiler doesn’t optimize away part of the intended test code based on context. For example:

fn contains(haystack: &[&str], needle: &str) -> bool {
    haystack.iter().any(|x| x == &needle)
}

pub fn benchmark() {
    let haystack = vec!["abc", "def", "ghi", "jkl", "mno"];
    let needle = "ghi";
    for _ in 0..10 {
        contains(&haystack, needle);
    }
}

The compiler could theoretically make optimizations like the following:

  • The needle and haystack do not change, move the call to contains outside the loop and delete the loop
  • Inline contains
  • needle and haystack have values known at compile time, contains is always true. Remove the call and replace with true
  • Nothing is done with the result of contains: delete this function call entirely
  • benchmark now has no purpose: delete this function

It is not likely that all of the above happens, but the compiler is definitely able to make some optimizations that could result in a very inaccurate benchmark. This is where black_box comes in:

use std::hint::black_box;

// Same `contains` function.
fn contains(haystack: &[&str], needle: &str) -> bool {
    haystack.iter().any(|x| x == &needle)
}

pub fn benchmark() {
    let haystack = vec!["abc", "def", "ghi", "jkl", "mno"];
    let needle = "ghi";
    for _ in 0..10 {
        // Force the compiler to run `contains`, even though it is a pure function whose
        // results are unused.
        black_box(contains(
            // Prevent the compiler from making assumptions about the input.
            black_box(&haystack),
            black_box(needle),
        ));
    }
}

This essentially tells the compiler to block optimizations across any calls to black_box. So, it now:

  • Treats both arguments to contains as unpredictable: the body of contains can no longer be optimized based on argument values
  • Treats the call to contains and its result as volatile: the body of benchmark cannot optimize this away

This makes our benchmark much more realistic to how the function would actually be used, where arguments are usually not known at compile time and the result is used in some way.

§How to use this

In practice, black_box serves two purposes:

  1. It prevents the compiler from making optimizations related to the value returned by black_box
  2. It forces the value passed to black_box to be calculated, even if the return value of black_box is unused
use std::hint::black_box;

let zero = 0;
let five = 5;

// The compiler will see this and remove the `* five` call, because it knows that multiplying
// any integer by 0 will result in 0.
let c = zero * five;

// Adding `black_box` here disables the compiler's ability to reason about the first operand in the multiplication.
// It is forced to assume that it can be any possible number, so it cannot remove the `* five`
// operation.
let c = black_box(zero) * five;

While most cases will not be as clear-cut as the above example, it still illustrates how black_box can be used. When benchmarking a function, you usually want to wrap its inputs in black_box so the compiler cannot make optimizations that would be unrealistic in real-life use.

use std::hint::black_box;

// This is a simple function that increments its input by 1. Note that it is pure, meaning it
// has no side-effects. This function has no effect if its result is unused. (An example of a
// function *with* side-effects is `println!()`.)
fn increment(x: u8) -> u8 {
    x + 1
}

// Here, we call `increment` but discard its result. The compiler, seeing this and knowing that
// `increment` is pure, will eliminate this function call entirely. This may not be desired,
// though, especially if we're trying to track how much time `increment` takes to execute.
let _ = increment(black_box(5));

// Here, we force `increment` to be executed. This is because the compiler treats `black_box`
// as if it has side-effects, and thus must compute its input.
let _ = black_box(increment(black_box(5)));

There may be additional situations where you want to wrap the result of a function in black_box to force its execution. This is situational though, and may not have any effect (such as when the function returns a zero-sized type such as () unit).

Note that black_box has no effect on how its input is treated, only its output. As such, expressions passed to black_box may still be optimized:

use std::hint::black_box;

// The compiler sees this...
let y = black_box(5 * 10);

// ...as this. As such, it will likely simplify `5 * 10` to just `50`.
let _0 = 5 * 10;
let y = black_box(_0);

In the above example, the 5 * 10 expression is considered distinct from the black_box call, and thus is still optimized by the compiler. You can prevent this by moving the multiplication operation outside of black_box:

use std::hint::black_box;

// No assumptions can be made about either operand, so the multiplication is not optimized out.
let y = black_box(5) * black_box(10);

During constant evaluation, black_box is treated as a no-op.