Skip to content

Commit 00d1a55

Browse files
committed
Initial text for benchmarking RFC
1 parent f337bea commit 00d1a55

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed

text/0000-benchmarking.md

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
- Feature Name: benchmarking
2+
- Start Date: 2018-01-11
3+
- RFC PR: (leave this empty)
4+
- Rust Issue: (leave this empty)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
This aims to stabilize basic benchmarking tools for a stable `cargo bench`
10+
11+
# Motivation
12+
[motivation]: #motivation
13+
14+
Benchmarking is important for maintaining good libraries. They give us a clear idea of performance tradeoffs
15+
and make it easier to pick the best library for the job. They also help people keep track of performance regressions,
16+
and aid in finding and fixing performance bottlenecks.
17+
18+
# Guide-level explanation
19+
[guide-level-explanation]: #guide-level-explanation
20+
21+
You can write benchmarks much like tests; using a `#[bench]` annotation in your library code or in a
22+
dedicated file under `benches/`. You can also use `[[bench]]` entries in your `Cargo.toml` to place
23+
it in a custom location.
24+
25+
26+
A benchmarking function looks like this:
27+
28+
```rust
29+
use std::test::Bencher;
30+
31+
#[bench]
32+
fn my_benchmark(bench: &mut Bencher) {
33+
let x = do_some_setup();
34+
bench.iter(|| x.compute_thing());
35+
x.teardown();
36+
}
37+
```
38+
39+
`Bencher::iter` is where the actual code being benchmarked is placed. It will run the
40+
test multiple times until it has a clear idea of what the average time taken is,
41+
and the variance.
42+
43+
The benchmark can be run with `cargo bench`.
44+
45+
To ensure that the compiler doesn't optimize things away, use `test::black_box`.
46+
The following code will show very little time taken because of optimizations, because
47+
the optimizer knows the input at compile time and can do some of the computations beforehand.
48+
49+
```rust
50+
use std::test::Bencher;
51+
52+
fn pow(x: u32, y: u32) -> u32 {
53+
if y == 0 {
54+
1
55+
} else {
56+
x * pow(x, y - 1)
57+
}
58+
}
59+
60+
#[bench]
61+
fn my_benchmark(bench: &mut Bencher) {
62+
bench.iter(|| pow(4, 30));
63+
}
64+
```
65+
66+
```
67+
running 1 test
68+
test my_benchmark ... bench: 4 ns/iter (+/- 0)
69+
70+
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out
71+
```
72+
73+
However, via `test::black_box`, we can blind the optimizer to the input values,
74+
so that it does not attempt to use them to optimize the code:
75+
76+
```rust
77+
#[bench]
78+
fn my_benchmark(bench: &mut Bencher) {
79+
let x = test::black_box(4);
80+
let y = test::black_box(30);
81+
bench.iter(|| pow(x, y));
82+
}
83+
```
84+
85+
```
86+
running 1 test
87+
test my_benchmark ... bench: 11 ns/iter (+/- 2)
88+
89+
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out
90+
```
91+
92+
Any result that is yielded from the callback for `Bencher::iter()` is also
93+
black boxed; otherwise, the compiler might notice that the result is unused and
94+
optimize out the entire computation.
95+
96+
In case you are generating unused values that do not get returned from the callback,
97+
use `black_box()` on them as well:
98+
99+
```rust
100+
#[bench]
101+
fn my_benchmark(bench: &mut Bencher) {
102+
let x = test::black_box(4);
103+
let y = test::black_box(30);
104+
bench.iter(|| {
105+
black_box(pow(y, x));
106+
pow(x, y)
107+
});
108+
}
109+
```
110+
111+
# Reference-level explanation
112+
[reference-level-explanation]: #reference-level-explanation
113+
114+
The bencher reports the median value and deviation (difference between min and max).
115+
Samples are winsorized, so extreme outliers get clamped.
116+
117+
Avoid calling `iter` multiple times in a benchmark; each call wipes out the previously
118+
collected data.
119+
120+
`cargo bench` essentially takes the same flags as `cargo test`, except it has a `--bench foo`
121+
flag to select a single benchmark target.
122+
123+
# Drawbacks
124+
[drawbacks]: #drawbacks
125+
126+
The reason we haven't stabilized this so far is basically because we're hoping to have a custom test
127+
framework system, so that the bencher can be written as a crate. This is still an alternative, though
128+
there has been no movement on this front in years.
129+
130+
# Rationale and alternatives
131+
[alternatives]: #alternatives
132+
133+
This design works. It doesn't give you fine grained tools for analyzing results, but it's
134+
a basic building block that lets one do most benchmarking tasks. The alternatives include
135+
a custom test/bench framework, which is much more holistic, or exposing more
136+
fundamental building blocks.
137+
138+
Another possible API would be one which implicitly handles the black boxing, something
139+
like
140+
141+
```rust
142+
let input1 = foo();
143+
let input2 = bar();
144+
bencher.iter(|(input1, input2)| baz(input1, input2), (input1, input2))
145+
```
146+
147+
This has problems with the types not being Copy, and it feels a bit less flexible.
148+
149+
# Unresolved questions
150+
[unresolved]: #unresolved-questions
151+
152+
- Should stuff be in `std::test` or a partially-stabilized `libtest`?
153+
- Should we stabilize any other `Bencher` methods (like `run_once`)?
154+
- Stable mchine-readable output for this would be nice, but can be done in a separate RFC.
155+

0 commit comments

Comments
 (0)