#301 Aug 6, 2026

301. ln_1p / exp_m1 — Adding 1.0 Destroys Your Tiny Float Before ln Ever Runs

(1.0 + x).ln() looks innocent, but for tiny x the 1.0 + step rounds away most of x’s digits before ln even sees them. ln_1p and exp_m1 do the + 1 internally, where nothing is lost.

Say you’re turning a tiny growth rate into a log-return:

1
2
3
4
5
6
let r = 1e-12_f64;

// 1.0 + r rounds away most of r
let naive = (1.0 + r).ln();
// ~1.0000889e-12 — 4 digits, then noise
assert!((naive - r).abs() > 1e-17);

The problem is spacing. Around 1.0, consecutive f64 values are about 2.2e-16 apart, so 1.0 + 1e-12 snaps to the nearest representable value — and the snap error (~9e-17 here) is enormous compared to r itself. ln then faithfully computes the log of the wrong number. You get 1.0000889e-12: four correct digits, then noise.

ln_1p computes ln(1 + x) without ever materializing 1 + x:

1
2
3
let precise = r.ln_1p();
// true value is r - r²/2 ≈ r - 5e-25
assert!((precise - r).abs() < 1e-20);

Full precision — the true answer differs from r only in the 25th decimal place, and ln_1p nails it.

The same trap exists in the other direction. e^x - 1 for tiny x:

1
2
3
4
5
6
7
// exp(r) ≈ 1.0000000000010000...
// subtracting 1.0 exposes the rounding
let naive = r.exp() - 1.0;
assert!((naive - r).abs() > 1e-17);

let precise = r.exp_m1(); // e^r - 1
assert!((precise - r).abs() < 1e-20);

And since they’re exact inverses, they round-trip cleanly:

1
2
let back = r.ln_1p().exp_m1();
assert!((back - r).abs() < 1e-26);

If x is comfortably large — say 0.1 and up — the naive forms are fine. But interest rates, probabilities, and per-step deltas live exactly in the tiny range where they aren’t. Both methods have been stable since Rust 1.0; they’re a rename away.

← Previous 300. atan2 — The Angle Formula That Knows Its Quadrant