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