-
Notifications
You must be signed in to change notification settings - Fork 156
Change runtime benchmark definition to always refer to a self-contained function #1470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,19 +30,24 @@ impl BenchmarkGroup { | |
} | ||
|
||
/// Registers a single benchmark. | ||
/// `benchmark_name` uniquely identifies the benchmark. | ||
/// `constructor` should return a closure that will be benchmarked. | ||
pub fn register<F: Fn() -> Bench + Clone + 'static, R, Bench: FnOnce() -> R + 'static>( | ||
&mut self, | ||
name: &'static str, | ||
benchmark_name: &'static str, | ||
constructor: F, | ||
) { | ||
// We want to type-erase the target `func` by wrapping it in a Box. | ||
let benchmark_func = Box::new(move || benchmark_function(constructor.clone())); | ||
let benchmark_def = BenchmarkWrapper { | ||
func: benchmark_func, | ||
}; | ||
if self.benchmarks.insert(name, benchmark_def).is_some() { | ||
panic!("Benchmark {} was registered twice", name); | ||
if self | ||
.benchmarks | ||
.insert(benchmark_name, benchmark_def) | ||
.is_some() | ||
{ | ||
panic!("Benchmark {} was registered twice", benchmark_name); | ||
} | ||
} | ||
|
||
|
@@ -102,19 +107,60 @@ impl BenchmarkGroup { | |
} | ||
|
||
/// Adds a single benchmark to the benchmark group. | ||
/// ```ignore | ||
/// use benchlib::define_benchmark; | ||
/// | ||
/// define_benchmark!(group, my_bench, { | ||
/// || do_something() | ||
/// }); | ||
/// The first argument should be the `BenchmarkGroup` into which will the benchmark be stored. | ||
/// The second argument should be the name of the benchmark (a string literal). | ||
/// The third argument should be an identifier of a function that will be benchmarked. | ||
/// | ||
/// If you do not provide any other arguments, the benchmarked function should not receive any | ||
/// parameters (it can return something though). | ||
/// | ||
/// If you provide a fourth argument, it should be an expression `init` that prepares | ||
/// input data for the benchmarked function. The function should then receive a single parameter of | ||
/// the same type as `init`. | ||
/// ``` | ||
/// use benchlib::{benchmark_group, define_benchmark}; | ||
/// | ||
/// fn calc_no_param() -> u32 { 1 + 1 } | ||
/// | ||
/// fn calc_param(a: u32) -> u32 { | ||
/// a + 1 | ||
/// } | ||
/// | ||
/// fn main() { | ||
/// benchmark_group(|group| { | ||
/// define_benchmark!(group, my_bench, calc_no_param); | ||
/// define_benchmark!(group, my_bench, calc_param, 1); | ||
/// }); | ||
/// } | ||
/// ``` | ||
#[macro_export] | ||
macro_rules! define_benchmark { | ||
($group:expr, $name:ident, $fun:expr) => { | ||
let func = move || $fun; | ||
$group.register(stringify!($name), func); | ||
}; | ||
// Version without init | ||
($group:expr, $name:literal, $fun:ident) => {{ | ||
let constructor = || { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm having trouble with all the nested closures. Are they all necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I wanted to achieve this: The second closure is needed to apply There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, please add more comments to the code explaining all this. |
||
move || { | ||
let ret = $fun(); | ||
|
||
// Try to avoid optimizing the return value out. | ||
$crate::benchmark::black_box(ret); | ||
} | ||
}; | ||
$group.register($name, constructor); | ||
}}; | ||
// Version with init | ||
($group:expr, $name:literal, $fun:ident, $init:expr) => {{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be possible to make this take any number of arguments with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to do it, but my macro-fu isn't on par 😆 I can make arbitrary number of arguments work, but I don't know how to avoid the tuples. Normally I would do something like this: ($group:expr, $name:literal, $fun:ident, $($init:expr),*) => {{
let constructor = || {
|| fun($($init),*);
}; But in this way the init expression evaluation would be measured by the harness, which I want to avoid. So instead, I'd like to do something like this: ($group:expr, $name:literal, $fun:ident, $($init:expr),*) => {{
let constructor = || {
let args = ($($init),*);
|| fun(*args);
}; With There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work? ($group:expr, $name:literal, $fun:ident, $($init:expr),*) => {{
let constructor = || {
|| fun($($init),*);
}; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That looks like the first example that I have sent in my previous commit I think. But in this case the init expression would be evaluated inside the measured closure, which we want to avoid. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. It makes me uncomfortable that the evaluation position is so subtle, and not visible from the call sites. E.g. this: define_benchmark!(group, "nbody_10k", calculate_nbody, nbody::init(10000)); has a very different behaviour to this: let init = nbody::init(10000);
define_benchmark!(group, "nbody_10k", calculate_nbody, init); Perhaps the macro syntax could be changed so it doesn't look like a normal call, something like this:
It's not great but at least makes it clearer that something non-standard is happening. But now I'm wondering if the macro is hiding too much subtle stuff :/ |
||
let constructor = || { | ||
let init = $init; | ||
move || { | ||
let ret = $fun(init); | ||
|
||
// Try to avoid optimizing the return value out. | ||
$crate::benchmark::black_box(ret); | ||
} | ||
}; | ||
$group.register($name, constructor); | ||
}}; | ||
} | ||
|
||
pub use define_benchmark; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,37 @@ | ||
use std::io::{BufRead, BufReader, Write}; | ||
use std::io::{BufRead, BufReader, Cursor, Write}; | ||
|
||
use snap::{read::FrameDecoder, write::FrameEncoder}; | ||
|
||
use benchlib::benchmark::{black_box, run_benchmark_group}; | ||
use benchlib::define_benchmark; | ||
use benchlib::{black_box, define_benchmark, run_benchmark_group}; | ||
|
||
const BYTES: usize = 64 * 1024 * 1024; | ||
// Inspired by https://github.com/rust-lang/rust/issues/102727 | ||
// The pattern we want is a BufReader which wraps a Read impl where one Read::read call will | ||
// never fill the whole BufReader buffer. | ||
fn read_buf( | ||
mut reader: BufReader<FrameDecoder<Cursor<Vec<u8>>>>, | ||
) -> BufReader<FrameDecoder<Cursor<Vec<u8>>>> { | ||
while let Ok(buf) = reader.fill_buf() { | ||
if buf.is_empty() { | ||
break; | ||
} | ||
black_box(buf); | ||
let len = buf.len(); | ||
reader.consume(len); | ||
} | ||
reader | ||
} | ||
|
||
fn main() { | ||
// Inspired by https://github.com/rust-lang/rust/issues/102727 | ||
// The pattern we want is a BufReader which wraps a Read impl where one Read::read call will | ||
// never fill the whole BufReader buffer. | ||
run_benchmark_group(|group| { | ||
define_benchmark!(group, bufreader_snappy, { | ||
let data = vec![0u8; BYTES]; | ||
move || { | ||
let mut compressed = Vec::new(); | ||
FrameEncoder::new(&mut compressed) | ||
.write_all(&data[..]) | ||
.unwrap(); | ||
let mut reader = | ||
BufReader::with_capacity(BYTES, FrameDecoder::new(&compressed[..])); | ||
define_benchmark!(group, "bufreader_snappy", read_buf, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a complex There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't be, and that is also the point of why the constructor exists. So that I can call the benchmark in a two part way:
And also ideally, then large init data could be returned from the function and dropped (or I think that criterion offers similar tradeoffs by letting users pass data by value or by reference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That all sounds reasonable, it's just not currently clear. More documentation please! :) |
||
const BYTES: usize = 64 * 1024 * 1024; | ||
|
||
while let Ok(buf) = reader.fill_buf() { | ||
if buf.is_empty() { | ||
break; | ||
} | ||
black_box(buf); | ||
let len = buf.len(); | ||
reader.consume(len); | ||
} | ||
} | ||
let data = vec![0u8; BYTES]; | ||
let mut compressed = Vec::new(); | ||
FrameEncoder::new(&mut compressed) | ||
.write_all(&data[..]) | ||
.unwrap(); | ||
BufReader::with_capacity(BYTES, FrameDecoder::new(Cursor::new(compressed))) | ||
}); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,27 @@ | ||
use benchlib; | ||
use benchlib::benchmark::run_benchmark_group; | ||
use benchlib::define_benchmark; | ||
use benchlib::{define_benchmark, run_benchmark_group}; | ||
use fxhash::FxBuildHasher; | ||
use hashbrown::HashMap; | ||
|
||
fn map_insert( | ||
(mut map, count): (HashMap<usize, usize, FxBuildHasher>, usize), | ||
) -> HashMap<usize, usize, FxBuildHasher> { | ||
for index in 0..count { | ||
map.insert(index, index); | ||
} | ||
map | ||
} | ||
|
||
fn main() { | ||
run_benchmark_group(|group| { | ||
// Measures how long does it take to insert 10 thousand numbers into a `hashbrown` hashmap. | ||
define_benchmark!(group, hashmap_insert_10k, { | ||
let mut map = hashbrown::HashMap::with_capacity_and_hasher( | ||
define_benchmark!(group, "hashmap_insert_10k", map_insert, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
( | ||
hashbrown::HashMap::with_capacity_and_hasher( | ||
10000, | ||
fxhash::FxBuildHasher::default(), | ||
), | ||
10000, | ||
fxhash::FxBuildHasher::default(), | ||
); | ||
move || { | ||
for index in 0..10000 { | ||
map.insert(index, index); | ||
} | ||
} | ||
) | ||
}); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,19 @@ | ||
//! Calculates the N-body simulation. | ||
//! Code taken from https://github.com/prestontw/rust-nbody | ||
|
||
use benchlib::benchmark::run_benchmark_group; | ||
use benchlib::define_benchmark; | ||
use benchlib::{define_benchmark, run_benchmark_group}; | ||
|
||
mod nbody; | ||
|
||
fn calculate_nbody(mut body: nbody::BodyStates) -> nbody::BodyStates { | ||
for _ in 0..3 { | ||
body = nbody::compute_forces(body); | ||
} | ||
body | ||
} | ||
|
||
fn main() { | ||
run_benchmark_group(|group| { | ||
define_benchmark!(group, nbody_10k, { | ||
let mut nbody_10k = nbody::init(10000); | ||
|| { | ||
for _ in 0..10 { | ||
nbody_10k = nbody::compute_forces(nbody_10k); | ||
} | ||
nbody_10k | ||
} | ||
}); | ||
define_benchmark!(group, "nbody_10k", calculate_nbody, nbody::init(10000)); | ||
}); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.