Files
improvise/crates/numtower/examples/bench.rs
T

188 lines
6.1 KiB
Rust

//! ADR 005 migration step 1's benchmark: `cargo run --release -p numtower --example bench`.
//! A microbenchmark, not the evaluator; what it does and does not show is in `context/code/numtower.md`.
use std::hint::black_box;
use std::time::Instant;
use numtower::{Decimal, ExactSum, Num, NumSum, Rounding, Typing, parse_number};
struct SplitMix64(u64);
impl SplitMix64 {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
/// Typed two-place values from 0.01 to 999,999.99, as the ADR's appendix draws them.
fn typed_texts(seed: u64, n: usize) -> Vec<String> {
let mut rng = SplitMix64(seed);
(0..n)
.map(|_| {
let cents = 1 + rng.next() % 99_999_999;
format!("{}.{:02}", cents / 100, cents % 100)
})
.collect()
}
fn nums(texts: &[String]) -> Vec<Num> {
texts
.iter()
.map(|t| parse_number(t, Typing::Inferred).unwrap())
.collect()
}
/// The best of `runs`, in nanoseconds per item.
fn best(runs: usize, items: usize, mut f: impl FnMut()) -> f64 {
(0..runs)
.map(|_| {
let t = Instant::now();
f();
t.elapsed().as_nanos() as f64 / items as f64
})
.fold(f64::INFINITY, f64::min)
}
fn sums() {
println!("SUM over typed two-place values, ns per element, best of 20");
for n in [18_000, 90_000] {
let texts = typed_texts(1, n);
let floats: Vec<f64> = texts.iter().map(|t| t.parse().unwrap()).collect();
let decimals: Vec<Decimal> = texts.iter().map(|t| t.parse().unwrap()).collect();
let values = nums(&texts);
let f64_fold = best(20, n, || {
black_box(black_box(&floats).iter().sum::<f64>());
});
let decimal_fold = best(20, n, || {
let s = black_box(&decimals)
.iter()
.fold(Decimal::ZERO, |a, b| a.checked_add(*b).unwrap());
black_box(s);
});
let num_fold = best(20, n, || {
let zero = Num::exact(Decimal::ZERO);
black_box(black_box(&values).iter().fold(zero, |a, b| &a + b));
});
let accumulator = best(20, n, || {
let mut s = NumSum::new();
black_box(&values).iter().for_each(|v| s.insert(v));
black_box(s.read());
});
println!(
" {n:>6} cells: f64 {f64_fold:.2}, rust_decimal {decimal_fold:.2}, Num + fold {num_fold:.2}, NumSum {accumulator:.2}"
);
}
}
fn operations() {
println!("Per operation over 18,000 typed pairs, ns, best of 20");
let (a, b) = (
nums(&typed_texts(21, 18_000)),
nums(&typed_texts(22, 18_000)),
);
let decimal =
|v: &[Num]| -> Vec<Decimal> { v.iter().map(|n| n.to_string().parse().unwrap()).collect() };
let (da, db) = (decimal(&a), decimal(&b));
let pairs = a.len();
let rd_add = best(20, pairs, || {
da.iter().zip(&db).for_each(|(x, y)| {
black_box(x.checked_add(*y));
})
});
let rd_mul = best(20, pairs, || {
da.iter().zip(&db).for_each(|(x, y)| {
black_box(x.checked_mul(*y));
})
});
let rd_div = best(20, pairs, || {
da.iter().zip(&db).for_each(|(x, y)| {
black_box(x.checked_div(*y));
})
});
let add = best(20, pairs, || {
a.iter().zip(&b).for_each(|(x, y)| drop(black_box(x + y)))
});
let mul = best(20, pairs, || {
a.iter().zip(&b).for_each(|(x, y)| drop(black_box(x * y)))
});
let div = best(20, pairs, || {
a.iter()
.zip(&b)
.for_each(|(x, y)| drop(black_box(x.checked_div(y))))
});
let margin = best(20, pairs, || {
a.iter()
.zip(&b)
.for_each(|(rev, cost)| drop(black_box((rev - cost).checked_div(rev))));
});
let to_f64 = best(20, pairs, || {
a.iter().for_each(|x| {
black_box(x.to_f64());
})
});
let places = best(20, pairs, || {
a.iter()
.for_each(|x| drop(black_box(x.round_to_places(2, Rounding::HalfAwayFromZero))));
});
println!(" rust_decimal checked: add {rd_add:.1}, mul {rd_mul:.1}, div {rd_div:.1}");
println!(" Num: add {add:.1}, mul {mul:.1}, div {div:.1}");
println!(
" Num (Rev - Cost) / Rev {margin:.1}, to_f64 {to_f64:.1}, round_to_places(2) {places:.1}"
);
}
/// K groups of near-`i128` denominators: the ADR's worst case for the exact read.
fn grouped_read() {
println!("NumSum read over K distinct 126-bit denominators, us per read, best of 200");
for k in [2usize, 8] {
let mut rng = SplitMix64(0x5EED);
let mut sum = NumSum::new();
for _ in 0..k {
let den = ((rng.next() as i128) << 62 | rng.next() as i128 >> 2) | 1 | (1 << 125);
let num = (rng.next() as i128) << 60 ^ rng.next() as i128;
let term = Num::ratio(num, den).unwrap();
sum.add(&term, i64::MAX);
sum.add(&term, i64::MAX);
}
assert_eq!(sum.groups(), k);
let read = best(200, 1, || drop(black_box(sum.read())));
println!(
" K = {k}: {:.1} us (exact: {})",
read / 1000.0,
sum.read().is_exact()
);
}
}
fn exact_sum() {
println!("ExactSum (f64 superaccumulator), best of 20");
let floats: Vec<f64> = typed_texts(1, 1_000_000)
.iter()
.map(|t| t.parse().unwrap())
.collect();
let mut acc = ExactSum::new();
let insert = best(20, floats.len(), || {
acc = ExactSum::new();
floats.iter().for_each(|x| acc.add(*x, 1));
});
let read = best(20, 1, || {
black_box(acc.value());
});
println!(
" {insert:.2} ns per insert, {:.2} us per read",
read / 1000.0
);
}
fn main() {
println!("numtower bench (release); name the machine when you quote these");
sums();
operations();
grouped_read();
exact_sum();
}