The numbers you do not have
Your machine cannot represent 0.1, cannot represent most of the reals, and its arithmetic is not associative. Everything else in this path is built on top of that.
Start with the thing everyone has seen and nobody is told the reason for.
0.1 + 0.2 === 0.3 // false
0.1 + 0.2 // 0.30000000000000004
The usual explanation — "floating point is inexact" — is true and useless. It suggests a small random fuzz sprinkled over otherwise-real numbers. What is actually happening is sharper than that, and once you see it, the rest of numerical computing stops being a list of gotchas and becomes a single consistent story.
A double is 64 bits: one sign bit, eleven exponent bits, and fifty-two mantissa bits,
encoding
That is a finite set. There are at most doubles, and the real line is not finite, so almost every real number you can name is simply not in the set. is not in the set — in binary it is the repeating fraction , and it gets rounded to the nearest available neighbour before your program does anything at all.
The relative gap stays near machine epsilon everywhere; it is the absolute gap that grows with magnitude. That is why error is naturally measured relatively.
Drag that up past and watch the last readout. The gap between neighbouring
doubles becomes larger than 1, which means consecutive integers stop existing:
x + 1 === x evaluates to true, and no error is raised. The number simply has
nowhere to go.
You add 1.0 to a double, one billion times in a loop, starting from 2^53. What do you end up with?
The relative gap is what stays constant, not the absolute one. That constant is machine epsilon: the distance from to the next representable double.
Roughly sixteen significant decimal digits — everywhere, at every magnitude. Which means the natural way to measure error is relative, and the natural way to lose accuracy is to do something that destroys significant digits.
The one operation that actually hurts
Multiplication and division are well behaved: relative errors add, slowly. Addition is fine too. Subtracting two nearly equal numbers is the operation that destroys you.
If and agree to twelve digits, then has only four meaningful digits left — and the arithmetic will hand you a result with sixteen digits printed, of which twelve are noise promoted to the front. Nothing warns you. The result looks fine.
Math.sqrt(x + 1) - Math.sqrt(x)1 / (Math.sqrt(x + 1) + Math.sqrt(x))Multiply by the conjugate: the subtraction disappears entirely. Both expressions are the same function on paper. They are not the same algorithm.
Both expressions in that panel are the same function. Not approximately the same — identically the same, provably, by algebra. Only one of them survives contact with a machine, because only one of them avoids forming a difference of near-equal quantities.
This is the distinction the whole field rests on:
- Conditioning is a property of the problem. Some questions genuinely amplify input error, and no algorithm can rescue them.
- Stability is a property of the algorithm. Two algorithms for the same well-conditioned problem can differ by ten orders of magnitude, as you just saw.
√(x+1) − √x lose accuracy for large x, and what is the fix?For large , and agree in almost every significant digit, so their difference cancels the leading digits and leaves rounding noise scaled up to the front of the answer.
The fix is to remove the subtraction algebraically, by multiplying through by the conjugate:
Now the only operations are addition, a square root, and a division — none of which cancel. The rewritten form is accurate to machine precision for every .
It is tempting to model rounding as noise. It is not noise — it is a deterministic function, and that has consequences.
IEEE-754 guarantees that each individual operation is correctly rounded: the result is the exact mathematical answer, rounded once to the nearest representable double. So for a single operation,
Every operation is individually near-perfect. Error analysis is entirely about how these terms compound, and the compounding is where the interesting behaviour lives.
One immediate consequence: floating-point addition is not associative.
(0.1 + 0.2) + 0.3 // 0.6000000000000001
0.1 + (0.2 + 0.3) // 0.6Which is not a bug — both results are correctly rounded. It means the order of summation is part of the algorithm. Summing a million values ascending, descending, or pairwise gives three different answers, and the spread between them can dwarf .
This is why Kahan summation exists: carry a running compensation term for the low -order bits discarded at each step, and the error stops growing with .
function kahanSum(xs) {
let sum = 0;
let c = 0; // the bits we dropped last time
for (const x of xs) {
const y = x - c; // put them back
const t = sum + y; // this addition loses the low bits of y...
c = (t - sum) - y; // ...and this recovers exactly what was lost
sum = t;
}
return sum;
}The line c = (t - sum) - y is exactly the kind of expression an optimising compiler
would love to simplify to zero. It is zero in real arithmetic. It is not zero in
floating point, and that residue is the entire point — which is why aggressive
fast-math flags break this function.
The 64-bit double is no longer the default everywhere it used to be.
- Mixed precision. Modern GPUs run
float16andbfloat16many times faster thanfloat64.bfloat16keeps all eight exponent bits of afloat32and throws away mantissa instead — it trades precision for range, because in practice overflow kills a training run and a few lost digits do not. Iterative refinement lets you do the bulk of the work in low precision and recover high-precision answers. - Posits and other alternative encodings vary precision with magnitude, giving more accuracy near 1 where most computation lives.
- Interval arithmetic carries a rigorous bracket around every value, so the computation reports its own uncertainty instead of leaving you to estimate it.
- Compensated algorithms (Kahan, Neumaier, two-sum) recover most of the lost
precision at a small constant cost, and are what good library
sumimplementations are quietly doing.
What to carry forward
Every method in this path is going to make errors. The useful question is never "is there error" — there always is — but which of the two kinds is dominant, and what happens to it next:
- Truncation error, from replacing a limit with a finite step. Shrinks as you refine.
- Roundoff error, from finite precision. Grows as you refine, because refining means more operations and smaller differences.
Those two pull in opposite directions. The next lesson is where they collide head-on, and where the collision produces the single most important picture in the subject.