Skip to main content

benchmark_simple/
lib.rs

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/// Options.
14#[derive(Clone, Debug)]
15pub struct Options {
16    /// Number of iterations to perform.
17    pub iterations: u64,
18    /// Number of warm-up iterations to perform.
19    pub warmup_iterations: u64,
20    /// Minimum number of samples to collect.
21    pub min_samples: usize,
22    /// Maximum number of samples to collect.
23    pub max_samples: usize,
24    /// Maximum RSD to tolerate (in 0...100).
25    pub max_rsd: f64,
26    /// Maximum benchmark duration time.
27    pub max_duration: Option<Duration>,
28    /// Verbose output
29    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/// A benchmark result.
52#[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    /// Returns the number of ticks.
73    pub fn ticks(&self) -> u64 {
74        self.elapsed.ticks()
75    }
76
77    /// Returns the elapsed time in seconds.
78    pub fn as_secs(&self) -> u64 {
79        self.elapsed.as_secs(&self.precision)
80    }
81
82    /// Returns the elapsed time in seconds (floating point).
83    pub fn as_secs_f64(&self) -> f64 {
84        self.elapsed.as_secs_f64(&self.precision)
85    }
86
87    /// Returns the elapsed time in milliseconds.
88    pub fn as_millis(&self) -> u64 {
89        self.elapsed.as_millis(&self.precision)
90    }
91
92    /// Returns the elapsed time in nanoseconds.
93    pub fn as_ns(&self) -> u64 {
94        self.elapsed.as_ns(&self.precision)
95    }
96
97    /// Compute the throughput for a given volume of data.
98    /// The volume is the amount of bytes processed in a single iteration.
99    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    /// Compute the throughput in bits for a given volume of data.
112    /// The volume is the amount of bytes processed in a single iteration.
113    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    /// Compute the throughput in bytes for a given volume of data.
121    /// The volume is the amount of bytes processed in a single iteration.
122    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/// Unit
142#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
143pub enum Unit {
144    /// None
145    #[default]
146    None,
147    /// Bytes
148    Bytes,
149    /// Bits
150    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/// The result of a benchmark, as a throughput.
164#[derive(Clone)]
165pub struct Throughput {
166    volume: f64,
167    result: BenchResult,
168    unit: Unit,
169}
170
171impl Throughput {
172    /// The throughput as a floating point number.
173    pub fn as_f64(&self) -> f64 {
174        self.volume * 1_000_000_000f64 / (max(1, self.result.as_ns()) as f64)
175    }
176
177    /// The throughput as an integer.
178    /// Saturates at `u128::MAX` if the rate cannot be represented.
179    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    /// The throughput in kibibytes.
189    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    /// The throughput in mebibytes.
194    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    /// The throughput in gibibytes.
199    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    /// The throughput in kilobytes.
206    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    /// The throughput in megabytes.
211    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    /// The throughput in gigabytes.
216    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    /// The throughput in kilobits.
223    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    /// The throughput in megabits.
228    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    /// The throughput in gigabits.
233    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/// A benchmarking environment.
272#[derive(Clone)]
273pub struct Bench {
274    precision: Precision,
275}
276
277impl Bench {
278    /// Create a new benchmarking environment.
279    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    /// Run a single test.
302    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/// Force the compiler to avoid optimizing away a value that is computed
376/// for benchmarking purposes, but not used afterwards.
377#[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}