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:
| |
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:
| |
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:
| |
And since they’re exact inverses, they round-trip cleanly:
| |
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.