edbb50c426
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
188 lines
6.1 KiB
Rust
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();
|
|
}
|