use std::time::{Duration, Instant};
pub use core::hint::black_box as slain;
pub struct Bencher {
iters: u64,
ticks: u64,
}
impl Bencher {
pub fn iter<R>(&mut self, mut f: impl FnMut() -> R) {
let t0 = clock::now();
for _ in 0..self.iters {
slain(f());
}
self.ticks = clock::now().wrapping_sub(t0);
}
}
type Arm<'a> = (String, Box<dyn FnMut(&mut Bencher) + 'a>);
pub struct Group<'a> {
name: String,
arms: Vec<Arm<'a>>,
cfg: Config,
}
#[derive(Clone, Copy)]
struct Config {
warm: Duration,
sample: Duration,
rounds: usize,
}
impl Default for Config {
fn default() -> Self {
Self { warm: Duration::from_millis(300), sample: Duration::from_micros(2000), rounds: 41 }
}
}
impl<'a> Group<'a> {
pub fn bench_function(&mut self, name: impl Into<String>, f: impl FnMut(&mut Bencher) + 'a) -> &mut Self {
self.arms.push((name.into(), Box::new(f)));
self
}
pub fn finish(mut self) {
if self.arms.is_empty() {
return;
}
let report = run(&self.name, &mut self.arms, self.cfg);
print!("{report}");
}
}
#[derive(Default)]
pub struct Harness {
filter: Option<String>,
}
impl Harness {
#[must_use]
pub fn from_args() -> Self {
pin_and_raise();
Self { filter: std::env::args().skip(1).find(|a| !a.starts_with('-')) }
}
pub fn benchmark_group<'a>(&mut self, name: impl Into<String>) -> Group<'a> {
let name = name.into();
let skip = self.filter.as_ref().is_some_and(|f| !name.contains(f.as_str()));
Group { name, arms: Vec::new(), cfg: if skip { Config { rounds: 0, ..Config::default() } } else { Config::default() } }
}
}
#[macro_export]
macro_rules! bench_main {
($($f:path),+ $(,)?) => {
fn main() {
let mut h = $crate::bench::Harness::from_args();
$($f(&mut h);)+
}
};
}
struct Measured {
name: String,
ns: Vec<f64>,
}
fn run(group: &str, arms: &mut [Arm<'_>], cfg: Config) -> String {
if cfg.rounds == 0 {
return String::new();
}
let mut slots: Vec<usize> = (0..arms.len()).collect();
slots.push(0);
let iters: Vec<u64> = (0..arms.len()).map(|a| calibrate(&mut arms[a].1, cfg.sample)).collect();
let t = Instant::now();
while t.elapsed() < cfg.warm {
for (a, arm) in arms.iter_mut().enumerate() {
sample(&mut arm.1, iters[a]);
}
}
let overhead = timer_overhead();
let mut data: Vec<Measured> = slots
.iter()
.enumerate()
.map(|(s, &a)| Measured { name: if s >= arms.len() { format!("{} (A/A control)", arms[a].0) } else { arms[a].0.clone() }, ns: Vec::with_capacity(cfg.rounds) })
.collect();
for round in 0..cfg.rounds {
for k in 0..slots.len() {
let s = (k + round) % slots.len();
let a = slots[s];
let ticks = sample(&mut arms[a].1, iters[a]).saturating_sub(overhead);
data[s].ns.push(clock::to_ns(ticks) / iters[a] as f64);
}
}
report(group, &data)
}
fn calibrate(f: &mut Box<dyn FnMut(&mut Bencher) + '_>, target: Duration) -> u64 {
let mut iters = 1u64;
loop {
let t = Instant::now();
sample(f, iters);
let e = t.elapsed();
if e >= target || iters >= 1 << 40 {
return iters;
}
let scale = (target.as_secs_f64() / e.as_secs_f64().max(1e-9)).clamp(1.5, 100.0);
iters = ((iters as f64) * scale).ceil() as u64;
}
}
fn sample(f: &mut Box<dyn FnMut(&mut Bencher) + '_>, iters: u64) -> u64 {
let mut b = Bencher { iters, ticks: 0 };
f(&mut b);
b.ticks
}
fn timer_overhead() -> u64 {
let mut best = u64::MAX;
for _ in 0..1000 {
let t0 = clock::now();
best = best.min(clock::now().wrapping_sub(t0));
}
best
}
fn median(v: &mut [f64]) -> f64 {
v.sort_by(f64::total_cmp);
let n = v.len();
if n == 0 {
f64::NAN
} else if n % 2 == 1 {
v[n / 2]
} else {
(v[n / 2 - 1] + v[n / 2]) / 2.0
}
}
fn bootstrap(x: &[f64]) -> (f64, f64) {
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let n = x.len();
let mut meds: Vec<f64> = (0..2000)
.map(|_| {
let mut s: Vec<f64> = (0..n).map(|_| x[(next() % n as u64) as usize]).collect();
median(&mut s)
})
.collect();
meds.sort_by(f64::total_cmp);
(meds[50], meds[1949])
}
fn report(group: &str, data: &[Measured]) -> String {
use std::fmt::Write;
let mut out = String::new();
let base = &data[0].ns;
let speed = |m: &Measured| -> Vec<f64> { m.ns.iter().zip(base).map(|(a, b)| b / a).collect() };
let control = data.iter().find(|m| m.name.ends_with("(A/A control)"));
let noise = control.map_or(0.0, |c| {
let (lo, hi) = bootstrap(&speed(c));
(1.0 - lo).abs().max((hi - 1.0).abs())
});
let _ = writeln!(out, "\n{group} (noise floor ±{:.9}%)", noise * 100.0);
for m in data {
let med = median(&mut m.ns.clone());
let r = speed(m);
let (lo, hi) = bootstrap(&r);
let rm = median(&mut r.clone());
let verdict = if std::ptr::eq(m, &data[0]) {
"baseline".to_string()
} else if lo > 1.0 + noise {
format!("faster {rm:.9}x")
} else if hi < 1.0 - noise {
format!("slower {:.9}x", 1.0 / rm)
} else {
"inside noise".to_string()
};
let _ = writeln!(out, " {:<34} {:>14} speed {:.9} [{:.9}, {:.9}] {verdict}", m.name, fmt_rate(1e9 / med), rm, lo, hi);
}
out
}
fn fmt_rate(per_s: f64) -> String {
if per_s >= 1e9 {
format!("{:.9} billion/s", per_s / 1e9)
} else if per_s >= 1e6 {
format!("{:.9} million/s", per_s / 1e6)
} else if per_s >= 1e3 {
format!("{:.9} thousand/s", per_s / 1e3)
} else {
format!("{per_s:.9} per second")
}
}
fn pin_and_raise() {
#[cfg(windows)]
{
#[link(name = "kernel32")]
extern "system" {
fn GetCurrentThread() -> *mut core::ffi::c_void;
fn SetThreadAffinityMask(thread: *mut core::ffi::c_void, mask: usize) -> usize;
fn SetThreadPriority(thread: *mut core::ffi::c_void, priority: i32) -> i32;
}
const THREAD_PRIORITY_HIGHEST: i32 = 2;
unsafe {
let t = GetCurrentThread();
SetThreadAffinityMask(t, 1usize << 2);
SetThreadPriority(t, THREAD_PRIORITY_HIGHEST);
}
}
}
mod clock {
use std::sync::OnceLock;
use std::time::Instant;
fn origin() -> Instant {
static O: OnceLock<Instant> = OnceLock::new();
*O.get_or_init(Instant::now)
}
#[inline(always)]
pub fn now() -> u64 {
#[cfg(target_arch = "x86_64")]
{
unsafe { core::arch::x86_64::_rdtsc() }
}
#[cfg(not(target_arch = "x86_64"))]
{
origin().elapsed().as_nanos() as u64
}
}
fn ns_per_tick() -> f64 {
static K: OnceLock<f64> = OnceLock::new();
*K.get_or_init(|| {
if cfg!(not(target_arch = "x86_64")) {
return 1.0;
}
let (t0, c0) = (origin().elapsed(), now());
let spin = Instant::now();
while spin.elapsed().as_millis() < 50 {
std::hint::spin_loop();
}
let (t1, c1) = (origin().elapsed(), now());
(t1 - t0).as_nanos() as f64 / c1.wrapping_sub(c0).max(1) as f64
})
}
pub fn to_ns(ticks: u64) -> f64 {
ticks as f64 * ns_per_tick()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_a_real_difference_and_passes_the_control() {
let v: Vec<u64> = (0..4096).collect();
let mut arms: Vec<Arm<'_>> = vec![
("sum".into(), Box::new(|b: &mut Bencher| b.iter(|| slain(&v).iter().sum::<u64>()))),
("sum x4".into(), Box::new(|b: &mut Bencher| b.iter(|| (0..4).map(|_| slain(&v).iter().sum::<u64>()).sum::<u64>()))),
];
let cfg = Config { warm: Duration::from_millis(20), sample: Duration::from_micros(200), rounds: 15 };
let out = run("test", &mut arms, cfg);
assert!(out.contains("slower"), "{out}");
assert!(out.contains("(A/A control)"), "{out}");
assert!(out.contains("/s"), "{out}");
}
}