1#![doc = include_str!("../README.md")]
2
3use std::cmp::max;
4use std::fmt::{self, Debug, Display, Formatter};
5use std::mem;
6use std::ops::Add;
7use std::ptr;
8use std::rc::Rc;
9use std::time::Duration;
10
11use precision::*;
12
13#[derive(Clone, Debug)]
15pub struct Options {
16 pub iterations: u64,
18 pub warmup_iterations: u64,
20 pub min_samples: usize,
22 pub max_samples: usize,
24 pub max_rsd: f64,
26 pub max_duration: Option<Duration>,
28 pub verbose: bool,
30}
31
32impl Default for Options {
33 fn default() -> Self {
34 let mut verbose = false;
35 std::env::var("BENCHMARK_VERBOSE")
36 .map(|_| verbose = true)
37 .ok();
38
39 Self {
40 iterations: 1,
41 warmup_iterations: 0,
42 min_samples: 3,
43 max_samples: 5,
44 max_rsd: 5.0,
45 verbose,
46 max_duration: None,
47 }
48 }
49}
50
51#[derive(Clone)]
53pub struct BenchResult {
54 elapsed: Elapsed,
55 precision: Precision,
56 options: Rc<Options>,
57}
58
59impl Add for BenchResult {
60 type Output = BenchResult;
61
62 fn add(self, other: BenchResult) -> Self::Output {
63 BenchResult {
64 elapsed: self.elapsed + other.elapsed,
65 precision: self.precision,
66 options: self.options,
67 }
68 }
69}
70
71impl BenchResult {
72 pub fn ticks(&self) -> u64 {
74 self.elapsed.ticks()
75 }
76
77 pub fn as_secs(&self) -> u64 {
79 self.elapsed.as_secs(&self.precision)
80 }
81
82 pub fn as_secs_f64(&self) -> f64 {
84 self.elapsed.as_secs_f64(&self.precision)
85 }
86
87 pub fn as_millis(&self) -> u64 {
89 self.elapsed.as_millis(&self.precision)
90 }
91
92 pub fn as_ns(&self) -> u64 {
94 self.elapsed.as_ns(&self.precision)
95 }
96
97 pub fn throughput(self, volume: u128) -> Throughput {
100 let iterations = self.options.iterations as u128;
101 let volume = volume
102 .checked_mul(iterations)
103 .map_or_else(|| volume as f64 * iterations as f64, |volume| volume as f64);
104 Throughput {
105 volume,
106 result: self,
107 unit: Unit::None,
108 }
109 }
110
111 pub fn throughput_bits(self, volume: u128) -> Throughput {
114 let mut throughput = self.throughput(volume);
115 throughput.volume *= 8.0;
116 throughput.unit = Unit::Bits;
117 throughput
118 }
119
120 pub fn throughput_bytes(self, volume: u128) -> Throughput {
123 let mut throughput = self.throughput(volume);
124 throughput.unit = Unit::Bytes;
125 throughput
126 }
127}
128
129impl Display for BenchResult {
130 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
131 write!(f, "{:.2}s", self.as_secs_f64())
132 }
133}
134
135impl Debug for BenchResult {
136 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
137 write!(f, "{}", self)
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
143pub enum Unit {
144 #[default]
146 None,
147 Bytes,
149 Bits,
151}
152
153impl Display for Unit {
154 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
155 match self {
156 Unit::None => write!(f, ""),
157 Unit::Bytes => write!(f, "B"),
158 Unit::Bits => write!(f, "b"),
159 }
160 }
161}
162
163#[derive(Clone)]
165pub struct Throughput {
166 volume: f64,
167 result: BenchResult,
168 unit: Unit,
169}
170
171impl Throughput {
172 pub fn as_f64(&self) -> f64 {
174 self.volume * 1_000_000_000f64 / (max(1, self.result.as_ns()) as f64)
175 }
176
177 pub fn as_u128(&self) -> u128 {
180 (self.volume as u128)
181 .checked_mul(1_000_000_000)
182 .map_or_else(
183 || self.as_f64() as u128,
184 |volume| volume / max(1, self.result.as_ns()) as u128,
185 )
186 }
187
188 pub fn as_kib(&self) -> f64 {
190 self.volume * 1_000_000_000f64 / (max(1, self.result.as_ns()) as f64) / 1024.0
191 }
192
193 pub fn as_mib(&self) -> f64 {
195 self.volume * 1_000_000_000f64 / (max(1, self.result.as_ns()) as f64) / (1024.0 * 1024.0)
196 }
197
198 pub fn as_gib(&self) -> f64 {
200 self.volume * 1_000_000_000f64
201 / (max(1, self.result.as_ns()) as f64)
202 / (1024.0 * 1024.0 * 1024.0)
203 }
204
205 pub fn as_kb(&self) -> f64 {
207 self.volume * 1_000_000_000f64 / (max(1, self.result.as_ns()) as f64) / 1000.0
208 }
209
210 pub fn as_mb(&self) -> f64 {
212 self.volume * 1_000_000_000f64 / (max(1, self.result.as_ns()) as f64) / (1000.0 * 1000.0)
213 }
214
215 pub fn as_gb(&self) -> f64 {
217 self.volume * 1_000_000_000f64
218 / (max(1, self.result.as_ns()) as f64)
219 / (1000.0 * 1000.0 * 1000.0)
220 }
221
222 pub fn as_kb8(&self) -> f64 {
224 self.volume * 8_000_000_000f64 / (max(1, self.result.as_ns()) as f64) / 1000.0
225 }
226
227 pub fn as_mb8(&self) -> f64 {
229 self.volume * 8_000_000_000f64 / (max(1, self.result.as_ns()) as f64) / (1000.0 * 1000.0)
230 }
231
232 pub fn as_gb8(&self) -> f64 {
234 self.volume * 8_000_000_000f64
235 / (max(1, self.result.as_ns()) as f64)
236 / (1000.0 * 1000.0 * 1000.0)
237 }
238}
239
240impl Display for Throughput {
241 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
242 match self.unit {
243 Unit::None => match self.as_u128() {
244 0..=999 => write!(f, "{:.2} /s", self.as_f64()),
245 1_000..=999_999 => write!(f, "{:.2} K/s", self.as_kb()),
246 1_000_000..=999_999_999 => write!(f, "{:.2} M/s", self.as_mb()),
247 _ => write!(f, "{:.2} G/s", self.as_gb()),
248 },
249 Unit::Bytes => match self.as_u128() {
250 0..=999 => write!(f, "{:.2} B/s", self.as_f64()),
251 1_000..=999_999 => write!(f, "{:.2} KB/s", self.as_kb()),
252 1_000_000..=999_999_999 => write!(f, "{:.2} MB/s", self.as_mb()),
253 _ => write!(f, "{:.2} GB/s", self.as_gb()),
254 },
255 Unit::Bits => match self.as_u128() {
256 0..=999 => write!(f, "{:.2} b/s", self.as_f64()),
257 1_000..=999_999 => write!(f, "{:.2} Kb/s", self.as_kb()),
258 1_000_000..=999_999_999 => write!(f, "{:.2} Mb/s", self.as_mb()),
259 _ => write!(f, "{:.2} Gb/s", self.as_gb()),
260 },
261 }
262 }
263}
264
265impl Debug for Throughput {
266 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
267 write!(f, "{}", self)
268 }
269}
270
271#[derive(Clone)]
273pub struct Bench {
274 precision: Precision,
275}
276
277impl Bench {
278 pub fn new() -> Self {
280 let precision = Precision::new(Default::default()).unwrap();
281 Bench { precision }
282 }
283
284 fn run_once<F, G>(&self, options: Rc<Options>, f: &mut F) -> BenchResult
285 where
286 F: FnMut() -> G,
287 {
288 let iterations = options.iterations;
289 let start = self.precision.now();
290 for _ in 0..iterations {
291 black_box(f());
292 }
293 let elapsed = self.precision.now() - start;
294 BenchResult {
295 elapsed,
296 precision: self.precision.clone(),
297 options,
298 }
299 }
300
301 pub fn run<F, G>(&self, options: &Options, mut f: F) -> BenchResult
303 where
304 F: FnMut() -> G,
305 {
306 let options = Rc::new(options.clone());
307 let max_samples = std::cmp::max(1, options.max_samples);
308 let verbose = options.verbose;
309
310 if verbose {
311 println!("Starting a new benchmark.");
312 if options.warmup_iterations > 0 {
313 println!("Warming up for {} iterations.", options.warmup_iterations);
314 }
315 }
316 for _ in 0..options.warmup_iterations {
317 black_box(f());
318 }
319 let mut results = Vec::with_capacity(max_samples);
320 let start = self.precision.now();
321 for i in 1..=max_samples {
322 if verbose {
323 println!("Running iteration {}.", i);
324 }
325 let result = self.run_once(options.clone(), &mut f);
326 results.push(result);
327 if let Some(max_duration) = options.max_duration {
328 let elapsed =
329 Duration::from_nanos((self.precision.now() - start).as_ns(&self.precision));
330 if elapsed >= max_duration {
331 if verbose {
332 println!("Timeout.");
333 }
334 break;
335 }
336 }
337 if results.len() <= 1 {
338 if verbose {
339 println!("Iteration {}: {}", i, results.last().unwrap());
340 }
341 continue;
342 }
343 let mean = results.iter().map(|r| r.as_secs_f64()).sum::<f64>() / results.len() as f64;
344 let std_dev = (results
345 .iter()
346 .map(|r| (r.as_secs_f64() - mean).powi(2))
347 .sum::<f64>()
348 / (results.len() - 1) as f64)
349 .sqrt();
350 let rsd = std_dev * 100.0 / mean;
351 if verbose {
352 println!("Iteration {}: {:.2}s ± {:.2}%", i, mean, rsd);
353 }
354 if i >= options.min_samples && rsd < options.max_rsd {
355 if verbose {
356 println!("Enough samples have been collected.");
357 }
358 break;
359 }
360 }
361 let result = results.into_iter().min_by_key(|r| r.as_ns()).unwrap();
362 if verbose {
363 println!("Result: {}", result);
364 }
365 result
366 }
367}
368
369impl Default for Bench {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375#[inline(never)]
378pub fn black_box<T>(dummy: T) -> T {
379 let ret = unsafe { ptr::read_volatile(&dummy) };
380 mem::forget(dummy);
381 ret
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use std::sync::OnceLock;
388
389 fn bench() -> Bench {
390 static PRECISION: OnceLock<Precision> = OnceLock::new();
391 Bench {
392 precision: PRECISION
393 .get_or_init(|| {
394 Precision::new(Config::default().setup_duration(Duration::from_secs(1)))
395 .unwrap()
396 })
397 .clone(),
398 }
399 }
400
401 fn result(iterations: u64) -> BenchResult {
402 BenchResult {
403 elapsed: Elapsed::new(),
404 precision: bench().precision,
405 options: Rc::new(Options {
406 iterations,
407 ..Options::default()
408 }),
409 }
410 }
411
412 #[test]
413 fn throughput_units_and_iteration_counts() {
414 let result = result(3);
415 let generic = result.clone().throughput(2);
416 let bytes = result.clone().throughput_bytes(2);
417 let bits = result.throughput_bits(2);
418 assert_eq!(generic.as_u128(), 6_000_000_000);
419 assert_eq!(bytes.as_u128(), generic.as_u128());
420 assert_eq!(bits.as_u128(), 8 * bytes.as_u128());
421 assert_eq!(bytes.to_string(), "6.00 GB/s");
422 assert_eq!(bits.to_string(), "48.00 Gb/s");
423 }
424
425 #[test]
426 fn large_volumes_do_not_overflow() {
427 let result = result(u64::MAX);
428 let expected = u128::MAX as f64 * u64::MAX as f64;
429 assert_eq!(result.clone().throughput(u128::MAX).volume, expected);
430 assert_eq!(result.clone().throughput_bytes(u128::MAX).volume, expected);
431 assert_eq!(result.throughput_bits(u128::MAX).volume, expected * 8.0);
432 }
433
434 #[test]
435 fn representable_products_keep_integer_precision() {
436 let volume = (1_u128 << 53) + 1;
437 let result = result(3);
438 assert_eq!(
439 result.clone().throughput(volume).volume,
440 (volume * 3) as f64
441 );
442 assert_eq!(
443 result.throughput_bits(volume).volume,
444 (volume * 3 * 8) as f64
445 );
446 }
447
448 #[test]
449 fn zero_volume_and_iterations_have_zero_throughput() {
450 for (iterations, volume) in [(u64::MAX, 0), (0, u128::MAX)] {
451 let result = result(iterations);
452 for throughput in [
453 result.clone().throughput(volume),
454 result.clone().throughput_bytes(volume),
455 result.throughput_bits(volume),
456 ] {
457 assert_eq!(throughput.as_f64(), 0.0);
458 assert_eq!(throughput.as_u128(), 0);
459 }
460 }
461 }
462
463 #[test]
464 fn integer_throughput_preserves_truncation() {
465 let volume = (1_u128 << 53) - 1;
466 assert_eq!(
467 result(1).throughput(volume).as_u128(),
468 volume * 1_000_000_000
469 );
470 let throughput = BenchResult {
471 elapsed: Elapsed::from_ticks(1_u64 << 32),
472 ..result(1)
473 }
474 .throughput(100_000_000);
475 let expected = 100_000_000_u128 * 1_000_000_000 / max(1, throughput.result.as_ns()) as u128;
476 assert_eq!(throughput.as_u128(), expected);
477 }
478
479 #[test]
480 fn integer_throughput_handles_large_intermediate_products() {
481 let throughput = result(1).throughput(1_u128 << 100);
482 assert_eq!(throughput.as_u128(), u128::MAX);
483 assert!(!throughput.to_string().is_empty());
484 let throughput = Throughput {
485 result: BenchResult {
486 elapsed: Elapsed::from_ticks(1_u64 << 60),
487 ..result(1)
488 },
489 ..throughput
490 };
491 let expected = throughput.as_f64() as u128;
492 assert!(expected > 0 && expected < u128::MAX);
493 assert_eq!(throughput.as_u128(), expected);
494 }
495
496 #[test]
497 fn zero_duration_stops_after_first_sample() {
498 let mut calls = 0;
499 bench().run(
500 &Options {
501 iterations: 1,
502 max_duration: Some(Duration::ZERO),
503 verbose: false,
504 ..Options::default()
505 },
506 || calls += 1,
507 );
508 assert_eq!(calls, 1);
509 }
510
511 #[test]
512 fn fractional_duration_stops_after_first_over_budget_sample() {
513 let mut calls = 0;
514 bench().run(
515 &Options {
516 iterations: 1,
517 max_samples: 4,
518 max_rsd: 0.0,
519 max_duration: Some(Duration::from_millis(20)),
520 verbose: false,
521 ..Options::default()
522 },
523 || {
524 calls += 1;
525 std::thread::sleep(Duration::from_millis(50));
526 },
527 );
528 assert_eq!(calls, 1);
529 }
530
531 #[test]
532 fn sample_and_warmup_counts_without_timeout() {
533 for max_samples in [0, 1, 4] {
534 let mut calls = 0;
535 bench().run(
536 &Options {
537 iterations: 3,
538 warmup_iterations: 2,
539 max_samples,
540 max_rsd: 0.0,
541 verbose: false,
542 ..Options::default()
543 },
544 || calls += 1,
545 );
546 assert_eq!(calls, 2 + 3 * max(1, max_samples));
547 }
548 }
549}