Skip to content

Commit f40945e

Browse files
committed
Add benchmarks for converting from/to (decimal) strings.
1 parent ff32488 commit f40945e

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ edition = "2021"
55

66
[dependencies]
77
bitflags = "1.3.2"
8+
9+
[dev-dependencies]
10+
criterion = { version = "0.5.1", features = ["html_reports"] }
11+
12+
[[bench]]
13+
name = "decimal"
14+
harness = false

benches/decimal.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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

Comments
 (0)