5.4.22 · D5Scientific Computing (Python)

Question bank — Floating point gotchas — catastrophic cancellation, associativity failure

2,320 words11 min readBack to topic
Figure — Floating point gotchas — catastrophic cancellation, associativity failure

The figure above is the mental picture behind almost every trap on this page: representable doubles are dots on a number line whose spacing widens as magnitude grows, snaps any real to the nearest dot, and ties go to the even dot.


True or false — justify

Float addition is commutative ().
True. The machine computes — round the exact sum — and before rounding, so both orders snap to the identical dot.
Float addition is associative ().
False. Each + applies to its intermediate result at a spacing set by that partial sum's magnitude, so a different grouping rounds at a different place — e.g. gives one way and the other.
Subtracting two nearly-equal doubles is itself an inaccurate operation.
False. By Sterbenz's lemma the subtraction of near-equal values is computed exactly (no rounding at all); the large relative error was already baked into the two stored operands — subtraction merely reveals it.
Machine epsilon is the smallest positive number a double can represent.
False. It is the gap between and the next double (). The smallest positive subnormal double is vastly smaller (); these are entirely different quantities.
Unit roundoff and machine epsilon are the same thing.
False. . Machine epsilon is the full step between adjacent doubles near ; round-to-nearest can only miss by half a step, so the error bound uses , not .
When a real number lands exactly halfway between two doubles, IEEE-754 rounds it up.
False. It uses round half to even: it picks the neighbour with a final mantissa bit, so ties don't systematically bias long computations upward.
If a formula is correct in real-number algebra, its Python translation is correct too.
False. Algebraic identity does not survive rounding: a mathematically exact formula (e.g. ) can lose all accuracy through Catastrophic cancellation even though the symbols are perfect.
0.1 + 0.2 == 0.3 is True in Python.
False. None of is exactly representable in binary; differs from by about , so == returns False.
Using float128 (extended precision) always fixes a cancelling formula.
False. Higher precision only delays catastrophe by a few digits; if the expression is ill-conditioned, the amplification factor still blows up. You must reformulate, not just widen the type.
The rounding error of storing a number is a fixed absolute amount.
False. The bound is relative: (with ), so the absolute error grows with the number's magnitude. That relativity is exactly why big-minus-big is dangerous.
Numbers below the smallest normal double just round to zero.
False. Between the smallest normal and true zero lie the subnormals (gradual underflow): the leading mantissa bit becomes , trading precision for reach so tiny results degrade smoothly instead of snapping to . Only below the smallest subnormal do you truly underflow to .
Kahan summation makes floating addition exact.
False. It reduces the total error to (roughly independent of ) by carrying the lost low-order bits, but each individual add still applies — it compensates, it does not eliminate.
Adding numbers from largest to smallest is the safe order.
False. It is usually the worst order: small terms get absorbed under the huge running sum. Adding smallest magnitudes first (or using Kahan compensated summation) is safer.

Spot the error

if variance == 0.0: skip() used to detect a degenerate dataset.
The variance computed as can round to a tiny negative or tiny positive number via cancellation, so == 0.0 both misses true zeros and mis-fires. Test variance <= tol with tol an absolute threshold scaled to the data, e.g. tol = 1e-12 * mean_x**2 (relative to the numbers' size, since variance has units of value²).
Computing directly for large .
For big the two roots are nearly equal, so their difference suffers catastrophic cancellation. Rewrite as — here the two roots are added; since both are positive there is no near-total cancellation in the denominator, so relative error stays .
Taking the "" root of straight from .
With , and are both and nearly equal, so the subtraction is catastrophic. Compute the same-sign root first, then get — see Quadratic formula numerical issues.
Approximating a derivative with a step "to be as accurate as possible".
then subtracts near-equals, so cancellation dominates and accuracy collapses. There is an optimal balancing truncation vs round-off error.
total = sum(prices) over a million small transactions plus one huge balance.
Naive summation lets each small price be absorbed by the large running total, so cents silently vanish (absorption). Sum small-to-large, or use Kahan; see Round-off error propagation.
math.log(1 + x) for very small x.
rounds the tiny x away (absorption), so the log of it is instead of . Use math.log1p(x), which is built to avoid the addition to .
Testing convergence with while error != 0: in an iterative solver.
Floating error rarely reaches exactly ; it wobbles near round-off level forever, so the loop may never stop. Use while error > tol:, choosing tol as atol + rtol*scale (an absolute floor plus a relative part) rather than a lone magic number.
Summing a huge cancelling series like for large term by term.
The alternating terms grow enormous then cancel down to a tiny result — catastrophic cancellation across the sum. Compute (all-positive terms) and take its reciprocal — but only for moderate : for large , overflows to inf and swallows the answer, so use math.exp(-x) (or range-reduction) directly instead.

Why questions

Why does subtracting near-equal numbers amplify relative error rather than create new error?
Both operands carry an absolute error ; the true difference is tiny, so dividing those fixed absolute errors by a near-zero result makes the relative error, , explode. No new bits are lost — the small answer just can't hide the old error.
Why is adding two same-sign near-equal numbers safe while subtracting them is not?
For with the result is as big as the inputs, so the fixed absolute errors stay small relative to it — the amplifier . Subtraction shrinks the result toward while the errors stay put, so the same errors become huge relative to the answer.
Why is multiplication/division "safe" while subtraction is "dangerous"?
In the relative errors merely add () and never divide by anything near zero, so the amplification factor stays . Subtraction can divide by a near-zero difference, making the factor unbounded.
Why does round to ?
Near magnitude the spacing between representable doubles is about , which is larger than ; the added falls below one gap, so snaps back to — it is absorbed.
Why does the smallest-first summation order help?
Small values accumulated together first grow into a magnitude comparable to the larger terms, so when they finally meet a big number they occupy representable bits instead of falling below its gap — fewer of them get absorbed.
Why does Kahan summation subtract (t - s) - y rather than just tracking y?
(t - s) is the part of y that actually landed in the sum after rounding; subtracting the full y isolates the negated dropped part, which is then fed back next iteration so no bits are permanently lost.
Why does IEEE-754 bother with round-to-even instead of always rounding halves up?
Always-up would add a tiny upward bias every time a tie occurs, and over a long sum those biases accumulate into a systematic drift. Round-to-even splits ties evenly so they cancel on average, keeping the bound unbiased.
Why can a mathematically perfect identity be numerically better than another?
The two are equal in exact arithmetic but have different conditioning: one may route the computation around a subtraction of near-equals while the other walks straight into it. Reformulating changes error behaviour, not the math — see Numerical stability and conditioning.
Why do we quote error relatively () rather than as one fixed number?
Floating point spaces numbers logarithmically — the gap scales with magnitude — so a bound that scales with (relative) is the honest, magnitude-independent description of accuracy.
Why is comparing floats with a tolerance the right fix instead of rounding both to fewer digits?
Tolerance abs(x-y) <= atol + rtol*max(|x|,|y|) respects the relative nature of float error and adapts to magnitude, whereas fixed-digit rounding either over-rejects large numbers or over-accepts small ones.

Edge cases

What does the amplification factor become when exactly?
It is — formally infinite — but if and are the same stored value, the result is exactly with zero relative error. The blow-up applies when they are nearly but not identically equal.
For the quadratic fix , what breaks if ?
One root is exactly ; then correctly, but you must also guard against (both roots zero, i.e. ) to avoid a division by zero.
What happens to as before reformulating?
For around , rounds to exactly , so and the ratio is — a total loss versus the true limit . The identity restores it.
Is the naive-sum error bound tight for all data?
No — it is a worst case. For same-sign well-scaled data actual error is far smaller; for badly ordered or cancelling data it can approach the bound. It signals that error grows with , which is what Kahan removes.
What is the result of 0.0 == -0.0 and does the sign matter?
It is True — positive and negative zero compare equal — yet they can behave differently downstream (e.g. 1/0.0 gives , 1/-0.0 gives ), so the sign of zero is a real edge case in division and branch cuts.
What happens when a computation drifts into the subnormal range, and what does it cost?
Results below the smallest normal double keep going as subnormals (gradual underflow) — you don't jump straight to — but each subnormal has fewer mantissa bits, so relative precision degrades and, on some hardware, subnormal arithmetic runs much slower (or traps). Below the smallest subnormal you underflow to outright.
How should you treat a "negative variance" of, say, from cancellation?
It is a rounding artifact, not a real negative; clamp it to (max(v, 0.0)) before feeding it to sqrt, otherwise sqrt returns nan and poisons everything after it.
Recall One-line summary to carry away
  • Big minus big → small answer, big error. ::: Relative error amplifier explodes; reformulate to avoid it.
  • vs ? ::: — half a gap, because round-to-nearest can only miss by half.
  • Order and brackets change float sums. ::: Rounding is per-operation; sum small-first or use Kahan.
  • == on floats is a trap. ::: Compare with a tolerance (math.isclose), atol + rtol style.