Cancellation takes the answer, not a digit
The advice is universal and the explanation attached to it is usually wrong. Do not subtract nearly equal numbers is sound; because the subtraction is inaccurate is not. Subtract two floating-point numbers that lie within a factor of two of each other and the result is exact — no rounding occurs, not approximately none, none. The theorem is Sterbenz’s and it falls straight out of the staircase in what a float can hold: both operands sit on the same grid, so their difference is a multiple of that grid’s spacing and is representable.
What the subtraction does is reveal what was already there.
The error was in the operands
Each operand arrived carrying an error of up to half a gap at its own magnitude. Suppose two numbers near 1 are each accurate to 10⁻¹⁶ — sixteen good digits, everything a double can offer. Subtract them and get 10⁻¹². The difference is exact. But the two errors of size 10⁻¹⁶ are still there, and they are now errors in a quantity of size 10⁻¹²: a relative error of 10⁻⁴.
Twelve significant digits vanished in one operation that made no error.
That is the whole mechanism, and it explains the two things about cancellation that are otherwise puzzling. It explains why more precision does not fix it — a quadruple-precision version of the same subtraction loses the same twelve digits, starting from thirty-four instead of sixteen. And it explains why the damage is unbounded: the number of digits lost is the number of leading digits the two operands share, which can be as many as they have.
The collapse to exactly zero deserves a moment. At x = 10⁻⁸, cos x differs from 1 by about 5·10⁻¹⁷ — less than half the gap above 1, which is 1.1·10⁻¹⁶. So the correctly rounded value of cos x is 1, and 1 − 1 is 0, and 0 divided by 10⁻¹⁶ is 0. Every operation was correctly rounded. The answer is off by a factor of infinity.
Why the rearrangement works
The two expressions are the same function. The identity 1 − cos x = 2 sin²(x/2) is exact, and substituting it changes nothing about the mathematics.
What it changes is where the small quantity is formed. In the first version, the small quantity 1 − cos x is produced by subtracting two numbers of size 1 — so it is born with an absolute error inherited from operands a hundred million times larger than itself. In the second, sin(x/2) is computed directly at its own tiny magnitude, and squaring it is a multiplication, which is relatively accurate. The small number is never assembled from large ones.
That is the general recipe, and it is the only one there is: never form a small quantity as the difference of large ones. Everything else is an instance.
- The quadratic formula, where one of the two roots is computed as −b + √(b²−4ac) with b large and positive. The fix is to compute the other root first, where the sum is safe, and get this one from the product of the roots.
- The sample variance, computed as the mean of the squares minus the square of the mean. Two large, nearly equal quantities; the answer can come out negative. The fix is to subtract the mean first and then sum the squares.
- log(1+x) and eˣ−1 for small x, which is why the standard libraries ship
log1pandexpm1as separate functions rather than expecting them to be composed.
Two that bite in practice
Both of the classic instances are worth doing with numbers rather than describing, because the size of the failure is the part that does not survive being described.
The quadratic formula. Solve x² + 10⁸x + 1 = 0 in double precision. The discriminant is 10¹⁶ − 4, whose square root is very slightly less than 10⁸, and the smaller root computed as (−b + √(b²−4c))/2 comes out as −7.4506·10⁻⁹. The true root is −10⁻⁸. That is a relative error of 25%, in double precision, from a formula every schoolchild is given, with no unusual inputs and nothing that looks like a large or small number anywhere in sight.
The repair is to compute the root that does not cancel — the one where −b and −√(b²−4c) have the same sign — and obtain the other from the fact that the product of the roots is c. That gives −10⁻⁸ to sixteen digits.
The one-pass variance. Take a thousand measurements clustered around 10⁶ and differing in their third decimal place. Their variance, computed as the mean of the squares minus the square of the mean, is −9.77·10⁻⁴. The variance of a real dataset is negative. The two-pass computation — subtract the mean first, then sum the squares — gives 3.99·10⁻⁶, which is the answer.
The one-pass form is attractive because it needs a single sweep and two accumulators, and it appears in a great deal of production code and in more than one hardware instruction set. It forms a small number as the difference of two quantities near 10¹², so it loses about twelve digits, and a double carries sixteen. On data with slightly less spread it returns zero variance; on this data it returns less than zero. Neither is signalled.
Cancellation inside an algorithm
Everything above is visible in a one-line formula, which makes it easy to teach and easy to believe the matter is settled. The harder case is cancellation buried inside an algorithm written by someone else, where there is no formula to inspect and the only sign is an answer that is wrong.
The Householder reflection is the cleanest example in this subject, and it is one of the reasons the QR factorisations on this site behave. Each step of the algorithm sends a column x to a multiple of the first coordinate axis by reflecting it in a plane. There are two such multiples, +‖x‖e₁ and −‖x‖e₁, and the vector defining the plane is v = x − αe₁ for whichever α is chosen.
Choose α with the same sign as x₁ and the first component of v is x₁ − ‖x‖: a difference of two nearly equal positive numbers whenever x already points mostly along e₁. The reflection is then defined by a direction computed from cancelled digits, and every subsequent operation of the factorisation inherits it.
The fix costs one sign flip. It is in every serious implementation and in almost no derivation, because the derivation is correct either way — this is precisely the pattern the thread identical algebra, different arithmetic collects, and the reason a site about this subject has to run the algorithms rather than describe them.
How to see it coming
There is a usable rule of thumb. If a computation forms a quantity s as a difference and the operands have magnitude m, the relative error in s is roughly u·m/|s|. The number of decimal digits lost is about log₁₀(m/|s|).
That is worth applying before writing the code rather than after debugging it. When computing a quantity expected to be around 10⁻⁶ by subtracting two things of size 1, the decision has already been made to lose six digits, and in single precision — which carries about seven — the decision has been made to keep one.
The same arithmetic explains why several thresholds on this site sit at √u rather than u. When a computation forms ε² and adds it to 1, the result is 1 exactly as soon as ε² falls below the gap, which happens at ε = √u: 1.5·10⁻⁸ in double, 2.4·10⁻⁴ in single. That is not cancellation in the subtraction sense — it is the same phenomenon on the other side, a small quantity annihilated by a large one during an addition.
The other direction: accumulation
Cancellation destroys the answer in one operation. Its slower relative destroys it in a million, and the mechanism is the mirror image — instead of a small quantity being formed from large ones, a small quantity is repeatedly added to a large one and disappears into the gap.
The order they are added in takes that apart. The reason it belongs beside cancellation is that both are consequences of the same single fact — the gap is proportional to the magnitude — and both are repaired by the same single move: keep the operands near the size of the answer.
That move is also why the matrix algorithms later on are arranged the way they are. A Householder reflection touches numbers of comparable size; the normal equations form a product of a matrix with itself, which is the opposite. It is not a coincidence that the second one is the one that fails.
The refusals
An essay that claims a form is unstable owes a demonstration that the check can tell the difference, and both directions are run on every build here.
The stable form’s error is required to stay below 10⁻¹⁵ across twelve orders of magnitude of x, with that tolerance bracketed: the measured noise floor is 1.1·10⁻¹⁶ and the smallest deviation that would mean a real defect is around 10⁻⁷, so a tolerance of 10⁻¹⁵ sits strictly between them and can fail in both directions. A test set above the failure would accept the bug it exists to catch; one set below the noise would fire on a correct computation.
And the naive form is required to fail: to have lost measurable accuracy by x = 10⁻⁵, four digits by 10⁻⁶, and everything by 10⁻⁸. If a change to the arithmetic ever made the naive version work, the build would stop, because the essay’s claim would no longer be true. That is the discipline the thread assertions that reject is about, and it costs three lines.
Why this is not a floating-point problem
It is tempting to file all of this under floating point is unreliable and move on. That reading is comfortable and it is wrong, and getting it right is the difference between fixing these failures and merely fearing them.
Consider what an infinitely precise computer would do with (1 − cos x)/x². It would return the right answer, because it would carry every digit. Now consider what it would do if the input x itself were known only to sixteen digits — which, for any measured quantity, it is. Then 1 − cos x is determined to sixteen digits of x, which for small x is far fewer digits of the answer, and the exact computer’s exact answer to the wrong input is no better than the inexact computer’s.
The distinction is the one the whole site is organised around. The naive expression is a bad algorithm, because there is no nearby x for which zero is the right answer, and that is fixable — the rearranged form is the fix. But a genuinely ill-conditioned problem stays hard however it is computed, and no rearrangement helps.
The condition number is an amplifier makes that second quantity precise. Symmetry is worth more than precision is the extreme case: a perturbation of 10⁻¹⁶ moving an eigenvalue by 10⁻², with no algorithm at fault and no arithmetic that could prevent it.
Where this goes
Cancellation is the smallest unit of the subject’s central story. A computation loses accuracy not because the arithmetic is sloppy but because the quantity being computed is hard to determine from the inputs — and the two are separately identifiable.
The exact answer to a nearby problem generalises that into the vocabulary the rest of this site uses. The relevant sentence in advance: the naive form of (1 − cos x)/x² is a backward unstable algorithm — it does not give the exact answer to any nearby problem, because at x = 10⁻⁸ there is no nearby x for which the answer is zero. The stable form is backward stable, and its accuracy is limited only by how sensitive the function itself is, which near zero is not very.
That distinction is the difference between an algorithm that can be fixed and a problem that cannot. Both appear on this site in quantity, and telling them apart is most of the skill — an answer that is known is the essay where the distinction can be made without any interpretation at all, because the true answer is available in exact rational arithmetic and the two contributions can simply be subtracted.
Two more consequences run ahead. Elimination without a row swap fails by producing a multiplier so large that the useful part of a later entry is annihilated during an addition — the same mechanism, arrived at by a different route. And classical Gram–Schmidt loses orthogonality because a projection coefficient computed against the original column differs from the one computed against the remainder by an amount that has fallen below the gap. Neither of those looks like cancellation in the source code. Both are.
One more place it hides
The last instance worth naming is the one that catches people who have internalised everything above, because it does not look like a subtraction at all.
Computing the angle between two nearly parallel vectors by acos(u·v/(‖u‖‖v‖)) is a cancellation.
The dot product of two nearly parallel unit vectors is nearly 1, the arccosine’s derivative is
infinite there, and the resulting angle can be wrong in its first digit. The stable form uses
atan2(‖u×v‖, u·v), which forms the small quantity directly rather than as a difference from one.
The same pattern governs computing a variance, a difference of squares, a determinant of a nearly singular matrix, and the discriminant of a nearly repeated root. In every case the diagnosis is the same question: is the answer small, and was it assembled from things that are not?
That question — and not a rule about which operations to avoid — is what generalises. Elimination is a sequence of choices and rank is a decision are both, underneath, about keeping the quantities being compared at comparable sizes.