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, mut volume: u128) -> Throughput {
100        volume *= self.options.iterations as u128;
101        Throughput {
102            volume: volume as f64,
103            result: self,
104            unit: Unit::None,
105        }
106    }
107
108    /// Compute the throughput in bits for a given volume of data.
109    /// The volume is the amount of bytes processed in a single iteration.
110    pub fn throughput_bits(self, mut volume: u128) -> Throughput {
111        volume *= self.options.iterations as u128;
112        volume *= 8;
113        Throughput {
114            volume: volume as f64,
115            result: self,
116            unit: Unit::Bits,
117        }
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, mut volume: u128) -> Throughput {
123        volume *= self.options.iterations as u128;
124        volume *= 8;
125        Throughput {
126            volume: volume as f64,
127            result: self,
128            unit: Unit::Bytes,
129        }
130    }
131}
132
133impl Display for BenchResult {
134    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
135        write!(f, "{:.2}s", self.as_secs_f64())
136    }
137}
138
139impl Debug for BenchResult {
140    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
141        write!(f, "{}", self)
142    }
143}
144
145/// Unit
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147#[derive(Default)]
148pub enum Unit {
149    /// None
150    #[default]
151    None,
152    /// Bytes
153    Bytes,
154    /// Bits
155    Bits,
156}
157
158impl Display for Unit {
159    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
160        match self {
161            Unit::None => write!(f, ""),
162            Unit::Bytes => write!(f, "B"),
163            Unit::Bits => write!(f, "b"),
164        }
165    }
166}
167
168
169/// The result of a benchmark, as a throughput.
170#[derive(Clone)]
171pub struct Throughput {
172    volume: f64,
173    result: BenchResult,
174    unit: Unit,
175}
176
177impl Throughput {
178    /// The throughput as a floating point number.
179    pub fn as_f64(&self) -> f64 {
180        self.volume * 1_000_000_000f64 / (max(1, self.result.as_ns()) as f64)
181    }
182
183    /// The throughput as an integer.
184    pub fn as_u128(&self) -> u128 {
185        self.volume as u128 * 1_000_000_000 / (max(1, self.result.as_ns()) as u128)
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 results.len() <= 1 {
328                if verbose {
329                    println!("Iteration {}: {}", i, results.last().unwrap());
330                }
331                continue;
332            }
333            let mean = results.iter().map(|r| r.as_secs_f64()).sum::<f64>() / results.len() as f64;
334            let std_dev = (results
335                .iter()
336                .map(|r| (r.as_secs_f64() - mean).powi(2))
337                .sum::<f64>()
338                / (results.len() - 1) as f64)
339                .sqrt();
340            let rsd = std_dev * 100.0 / mean;
341            if verbose {
342                println!("Iteration {}: {:.2}s ± {:.2}%", i, mean, rsd);
343            }
344            if i >= options.min_samples && rsd < options.max_rsd {
345                if verbose {
346                    println!("Enough samples have been collected.");
347                }
348                break;
349            }
350            if let Some(max_duration) = options.max_duration {
351                let elapsed =
352                    Duration::from_secs((self.precision.now() - start).as_secs(&self.precision));
353                if elapsed >= max_duration {
354                    if verbose {
355                        println!("Timeout.");
356                    }
357                    break;
358                }
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}