Floating Point Arithmetic
What happens when you actually add and multiply rounded numbers: tiny errors that cancel, accumulate or explode depending on the order of operations.
Why Does This Exist?
Formats decide which numbers exist; arithmetic decides what happens when you compute with them. Every operation rounds its true result to the nearest representable neighbour, and those roundings interact: sometimes they cancel harmlessly, sometimes they accumulate into a wrong answer with a confident face. Floating point arithmetic is the study of that interaction: rounding modes, machine epsilon, catastrophic cancellation and summation order.
The two facts that organise everything: relative error per operation stays near machine epsilon ( in double precision), but absolute error scales with operand size. So adding a tiny number to a huge one rounds the tiny one away entirely, while subtracting nearly equal numbers amplifies the rounding both already carried. Formats are taught in floating point formats; the resulting failure catalogue in numerical stability.
The one idea you need first
A double-precision number carries about 16 significant decimal digits. Machine epsilon is the gap between 1 and the next double above it. Any operation on numbers near 1 rounds to multiples of ; near , consecutive doubles sit 2 apart and fractional parts stop existing.
Think of It Like This
A cash register that rounds every line
A register rounds each line item to the nearest dollar as it scans, then totals the rounded lines. A 49-cent pack of gum scanned beside a 2,000-dollar television contributes nothing: swallowed by the rounding of its line.
Scan gum with gum first and the cents accumulate into dollars before meeting the television, surviving the rounding. Same items, different order, different total. Catastrophic cancellation is the mirror image: two nearly equal totals subtracted, where the big matching dollars cancel and the total is built entirely from accumulated rounding dust.
How It Actually Works
Three mechanisms cover most incidents:
- Absorption: rounds to when . In doubles, , and . Small change dies beside big totals.
- Catastrophic cancellation: cancels the 's and leaves rounding noise promoted to the full answer. Relative error explodes even though every operation rounded correctly.
- Summation order: adding numbers smallest-first (or pairwise, or with Kahan compensation) keeps small contributions alive. Error grows like ordered well, typically random, and arbitrarily badly adversarially.
Worked example
The classic one-liner in double precision:
Neither 0.1 nor 0.2 exists in binary: they round to the nearest doubles, and those roundings ( and ) survive the addition, then survive the subtraction because 0.3 rounds slightly differently. The residue is pure rounding archaeology. Meanwhile : absorption kills the before subtraction even sees it, since .
Code
The point is the check: confirm both phenomena in three lines before designing any float comparison.
print((0.1 + 0.2) - 0.3) # -> 5.551115123125783e-17print((0.1 + 0.2) == 0.3) # -> Falseprint((1 + 1e-16) - 1) # -> 0.0Equality on floats is almost always wrong; compare with a tolerance (abs(a - b) <= 1e-9), and add small numbers first when a sum must keep them.
Watch Out For
Testing float equality with ==
Loss comparisons, convergence checks and unit tests written as == on floats fail on rounding dust like the above. They pass on the author's machine and fail in CI under a different BLAS ordering. Use tolerances everywhere: relative for scale-free checks, absolute near zero, and never exact equality past arithmetic.
Shrinking finite-difference steps until the check breaks
Gradient checks subtract nearly equal function values, which is catastrophic cancellation by design. Below in doubles, rounding noise dominates the difference and the check fails on correct gradients. The error is U-shaped in : truncation falls while round-off rises, so park near the bottom and use relative tolerances.
The Quick Version
- Every operation rounds; relative error stays near epsilon but absolute error scales with size.
- leaves : rounding residue, not a bug.
- absorbs to 1 in doubles: small change dies beside big totals.
- Add smallest-first (or compensate) to keep small contributions alive in long sums.
- Never compare floats with ==; gradient-check near , never far below.