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:
Claude (numtower)
2026-10-08 22:07:44 -07:00
committed by Edward Langley
parent df6b80842f
commit edbb50c426
7 changed files with 796 additions and 10 deletions
+187 -1
View File
@@ -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();
}
+17 -9
View File
@@ -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
+2
View File
@@ -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")));
}
// ---------------------------------------------------------------------------