|
| 1 | +//! Benchmarks for converting from/to (decimal) strings, the only operations |
| 2 | +//! that (may) need to allocate, and also some of the few that aren't `O(1)` |
| 3 | +//! (alongside e.g. div/mod, but even those likely have a better bound). |
| 4 | +
|
| 5 | +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; |
| 6 | +use std::fmt::{self, Write as _}; |
| 7 | + |
| 8 | +struct Sample { |
| 9 | + name: &'static str, |
| 10 | + decimal_str: &'static str, |
| 11 | +} |
| 12 | + |
| 13 | +impl fmt::Display for Sample { |
| 14 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 15 | + // HACK(eddyb) this is mostly to sort criterion's output correctly. |
| 16 | + write!(f, "[len={:02}] ", self.decimal_str.len())?; |
| 17 | + f.write_str(self.decimal_str)?; |
| 18 | + if !self.name.is_empty() { |
| 19 | + write!(f, " aka {}", self.name)?; |
| 20 | + } |
| 21 | + Ok(()) |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +impl Sample { |
| 26 | + const fn new(decimal_str: &'static str) -> Self { |
| 27 | + Self { name: "", decimal_str } |
| 28 | + } |
| 29 | + |
| 30 | + const fn named(self, name: &'static str) -> Self { |
| 31 | + Self { name, ..self } |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +const DOUBLE_SAMPLES: &[Sample] = &[ |
| 36 | + Sample::new("0.0"), |
| 37 | + Sample::new("1.0"), |
| 38 | + Sample::new("1234.56789"), |
| 39 | + Sample::new("3.14159265358979323846264338327950288").named("π"), |
| 40 | + Sample::new("0.693147180559945309417232121458176568").named("ln(2)"), |
| 41 | +]; |
| 42 | + |
| 43 | +fn double_from_str(c: &mut Criterion) { |
| 44 | + let mut group = c.benchmark_group("Double::from_str"); |
| 45 | + for sample in DOUBLE_SAMPLES { |
| 46 | + group.bench_with_input(BenchmarkId::from_parameter(sample), sample.decimal_str, |b, s| { |
| 47 | + b.iter(|| s.parse::<rustc_apfloat::ieee::Double>().unwrap()); |
| 48 | + }); |
| 49 | + } |
| 50 | + group.finish(); |
| 51 | +} |
| 52 | + |
| 53 | +/// `fmt::Write` implementation that does not need to allocate at all, |
| 54 | +/// but instead asserts that what's written matches a known string exactly. |
| 55 | +struct CheckerFmtSink<'a> { |
| 56 | + remaining: &'a str, |
| 57 | +} |
| 58 | + |
| 59 | +impl fmt::Write for CheckerFmtSink<'_> { |
| 60 | + fn write_str(&mut self, s: &str) -> fmt::Result { |
| 61 | + self.remaining = self.remaining.strip_prefix(s).ok_or(fmt::Error)?; |
| 62 | + Ok(()) |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +impl CheckerFmtSink<'_> { |
| 67 | + fn finish(self) -> fmt::Result { |
| 68 | + if self.remaining.is_empty() { |
| 69 | + Ok(()) |
| 70 | + } else { |
| 71 | + Err(fmt::Error) |
| 72 | + } |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +fn double_to_str(c: &mut Criterion) { |
| 77 | + let mut group = c.benchmark_group("Double::to_str"); |
| 78 | + for sample in DOUBLE_SAMPLES { |
| 79 | + let value = sample.decimal_str.parse::<rustc_apfloat::ieee::Double>().unwrap(); |
| 80 | + |
| 81 | + // `CheckerFmtSink` is used later to ensure the formatting doesn't get |
| 82 | + // optimized away, but without allocating - we can, however, allocate |
| 83 | + // the expected output here, ahead of time, and also sanity-check it |
| 84 | + // in a more convenient (and user-friendly) way, ensuring that benching |
| 85 | + // itself never panics (though not in a way the optimizer would know of). |
| 86 | + let value_to_string = &value.to_string(); |
| 87 | + |
| 88 | + // NOTE(eddyb) we only check that we get back the same floating-point |
| 89 | + // `value`, without comparing `value_to_string` and `sample.decimal_str`, |
| 90 | + // because `rustc_apfloat` (correctly) considers "natural precision" can |
| 91 | + // be shorter than our samples, and also it always strips trailing `.0` |
| 92 | + // (outside of scientific notation) - while it is possible to approximate |
| 93 | + // "is this plausibly close enough", it's an irrelevant complication here. |
| 94 | + assert_eq!(value_to_string.parse::<rustc_apfloat::ieee::Double>().unwrap(), value); |
| 95 | + |
| 96 | + group.bench_with_input( |
| 97 | + BenchmarkId::from_parameter(sample), |
| 98 | + &(value, value_to_string), |
| 99 | + |b, &(value, sample_to_string)| { |
| 100 | + b.iter(|| { |
| 101 | + let mut checker = CheckerFmtSink { |
| 102 | + remaining: sample_to_string, |
| 103 | + }; |
| 104 | + write!(checker, "{value}").unwrap(); |
| 105 | + checker.finish().unwrap(); |
| 106 | + }); |
| 107 | + }, |
| 108 | + ); |
| 109 | + } |
| 110 | + group.finish(); |
| 111 | +} |
| 112 | + |
| 113 | +criterion_group!(benches, double_from_str, double_to_str); |
| 114 | +criterion_main!(benches); |
0 commit comments