feat(numtower): benchmark, wasm CI entry, and the reasoning and evidence docs (improvise-59nw.1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
df6b80842f
commit
edbb50c426
@@ -1 +1,187 @@
|
||||
fn main() {}
|
||||
//! 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();
|
||||
}
|
||||
|
||||
@@ -165,23 +165,31 @@ impl NumSum {
|
||||
|
||||
/// Adds `x` with a Z-set `weight`. Exact and associative: the order cannot matter.
|
||||
pub fn add(&mut self, x: &Num, weight: i64) {
|
||||
match (x.kind(), x.exact_parts()) {
|
||||
(NumRef::Inexact(f), _) => {
|
||||
match x.kind() {
|
||||
NumRef::Inexact(f) => {
|
||||
self.inexact.add(f, weight);
|
||||
self.inexact_weight += weight as i128;
|
||||
}
|
||||
(_, Some((n, d))) => match at_scale_28(d) {
|
||||
Some(unit) => self.add_scaled(n, unit, weight),
|
||||
None => self.add_to_group(n, d, weight),
|
||||
NumRef::Exact(d) => {
|
||||
let unit = pow10(MAX_SCALE - d.scale()) as u128;
|
||||
self.add_scaled(d.mantissa(), unit, weight);
|
||||
}
|
||||
NumRef::Fraction(r) => match at_scale_28(*r.denom()) {
|
||||
Some(unit) => self.add_scaled(*r.numer(), unit, weight),
|
||||
None => self.add_to_group(*r.numer(), *r.denom(), weight),
|
||||
},
|
||||
(_, None) => unreachable!("an exact value has parts"),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_scaled(&mut self, n: i128, unit: u128, weight: i64) {
|
||||
let term = I320::from_i128(n)
|
||||
.checked_mul_u128(unit)
|
||||
.and_then(|t| t.checked_mul_u128(weight.unsigned_abs() as u128))
|
||||
let magnitude = weight.unsigned_abs() as u128;
|
||||
let scaled = match unit.checked_mul(magnitude) {
|
||||
Some(both) => I320::from_i128(n).checked_mul_u128(both),
|
||||
None => I320::from_i128(n)
|
||||
.checked_mul_u128(unit)
|
||||
.and_then(|t| t.checked_mul_u128(magnitude)),
|
||||
};
|
||||
let term = scaled
|
||||
.and_then(|t| if weight < 0 { t.checked_neg() } else { Some(t) })
|
||||
.expect("a weighted term is below 2^284");
|
||||
self.decimals = self
|
||||
|
||||
@@ -219,6 +219,8 @@ fn pow_decides_its_size_before_computing_and_honours_the_ceiling() {
|
||||
assert!(dec("0").pow(&dec("-1")).is_err());
|
||||
assert!(!dec("2").pow(&dec("0.5")).unwrap().is_exact());
|
||||
assert!(same(&dec("2").pow(&dec("-2")).unwrap(), &dec("0.25")));
|
||||
assert!(same(&dec("1.50").pow(&dec("2.0")).unwrap(), &dec("2.2500")));
|
||||
assert!(same(&dec("0").pow(&dec("0")).unwrap(), &dec("1")));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user