Foundationsprecisionroundingepsilonaccumulationequality

Why 0.1 + 0.2 Is Not 0.2 + 0.1's Problem

The famous result is not a bug and not a rounding display quirk. 0.1 has no exact binary representation for the same reason 1/3 has no exact decimal one, and every consequence — failed equality tests, drifting sums, order-dependent results — follows from that single fact.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
Why does `0.1 + 0.2` not equal `0.3`, and what should I actually do about it?
What you wrote
The computer got a simple sum wrong. Presumably there is a rounding setting, or a more precise type, that makes it right.
What the hardware does
Neither 0.1 nor 0.2 was ever stored. Each was rounded to the nearest representable binary value on the way in, the FPU added those two approximations exactly, and the result was rounded again. Every step was correct; the inputs were not the numbers you wrote.
Understanding this converts a mysterious class of bug into a predictable one. You stop trying to make floating point exact and start choosing between the three real options: tolerate the error with a sensible comparison, control it with a better summation order, or eliminate it by not using binary floating point for that quantity.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

The number was wrong before the addition happened

In decimal, 1/3 cannot be written exactly with finitely many digits — 0.333… repeats forever, so any stored version is truncated. Binary has the same limitation with a different set of victims. A fraction is exactly representable in binary only if its denominator is a power of two, so 0.5, 0.25 and 0.75 are exact while 0.1, 0.2 and 0.3 are not: in binary, 0.1 is 0.0001100110011… repeating.

So when source says 0.1, what is stored is the nearest double to 0.1, which is very slightly larger. Same for 0.2. The FPU then adds those two stored values with complete accuracy and rounds the exact sum to the nearest double — which happens to be very slightly above the nearest double to 0.3. Three correct roundings produce an inequality.

The clinching detail is that 0.1 + 0.2 === 0.3 is false while 0.5 + 0.25 === 0.75 is true, on the same hardware, with the same operations. Nothing about the arithmetic differs; only whether the inputs were representable.

What is actually stored, printed to enough digits to show it
Written        Nearest double (20 significant digits)
0.1            0.10000000000000000555111512312578
0.2            0.20000000000000001110223024625157
0.3            0.29999999999999998889776975374843

0.1 + 0.2  ->  0.30000000000000004440892098500626
0.3        ->  0.29999999999999998889776975374843
               ^ different values, so === is false

Exactly representable, because the denominators are powers of two:
0.5    = 2^-1              exact
0.25   = 2^-2              exact
0.75   = 2^-1 + 2^-2       exact
0.5 + 0.25 === 0.75        true

0.1 in binary is 0.0001100110011001100... repeating forever,
the same way 1/3 is 0.333... forever in decimal.

Comparison: absolute tolerance is not enough

The standard advice is to compare with a tolerance rather than for equality, and it is right as far as it goes. The trap is choosing a fixed absolute tolerance, because as Floating Point: Trading Precision for Range shows, the spacing between representable values grows with magnitude. A tolerance of 1e-9 is far too loose near 1e-15 and far too tight near 1e9, where consecutive doubles may be further apart than that.

The more robust form is a relative tolerance, scaled by the magnitude of the values being compared, usually combined with a small absolute floor so that comparisons against zero still work — a relative tolerance is meaningless when one side is zero.

The honest caveat is that any tolerance encodes a judgement about how much error your computation is expected to accumulate, and there is no universal value. Getting that judgement right requires knowing roughly how many operations contributed, which is why the summation question below matters.

Exact equality, or a fixed absolute epsilon
1if (a === b) { /* essentially never true for computed floats */ }
2
3const EPS = 1e-9
4if (Math.abs(a - b) < EPS) { /* ... */ }
5// Near 1e-15: absurdly loose -- treats unrelated values as equal.
6// Near 1e9: absurdly tight -- adjacent doubles differ by more
7// than 1e-9 up there, so it can never be true.
Relative tolerance with an absolute floor
1function closeEnough(a: number, b: number,
2 rel = 1e-9, abs = 1e-12): boolean {
3 const diff = Math.abs(a - b)
4 if (diff <= abs) return true // handles the zero case
5 return diff <= rel * Math.max(Math.abs(a), Math.abs(b))
6}
7// Scales with magnitude, so it behaves sensibly at 1e-15 and 1e9
8// alike. 'rel' still encodes how much accumulated error you expect,
9// which is a judgement about your computation, not a constant.

The absolute-epsilon version is testing a fixed distance against a spacing that varies over roughly thirty orders of magnitude. The relative version compares like with like. Neither makes the arithmetic exact — they choose a threshold below which you have decided the difference does not matter.

Accumulation, and why addition is not associative

Each operation rounds, so errors accumulate across a long computation. The effect is worst when values of very different magnitudes are combined: adding a tiny value to a large running total can be a complete no-op, because the exact result rounds straight back to the total. Sum a million small numbers into a large accumulator and a substantial fraction of them may contribute nothing at all.

This is why floating-point addition is not associative: (a + b) + c and a + (b + c) can genuinely differ. That has a direct practical consequence for parallelism — a parallel reduction regroups the additions by construction, so a multi-threaded sum can produce a different result from the sequential one, and different results run to run as the thread scheduling varies. That is not a bug in the reduction; it is the format.

The mitigations are ordinary engineering. Sorting smallest-first keeps the accumulator near the magnitude of what is being added. Compensated summation tracks the lost low-order bits in a second variable and folds them back in, recovering most of the accuracy for a modest constant cost. Using a wider accumulator than the data is often the simplest fix of all.

Three summations of the same data, three different answers
1const values = [1e16, 1.0, 1.0, -1e16]
2
3// Left to right: 1e16 + 1 rounds straight back to 1e16,
4// because 1 is far below the spacing of doubles at that magnitude.
5let naive = 0
6for (const v of values) naive += v
7// naive === 0 -- both 1.0 values vanished entirely
8
9// Rearranged so the small values meet each other first:
10const reordered = (values[1] + values[2]) + (values[0] + values[3])
11// reordered === 2 -- the answer you expected
12
13// Kahan compensated summation: carry the lost low-order bits.
14function kahanSum(xs: number[]): number {
15 let sum = 0, c = 0
16 for (const x of xs) {
17 const y = x - c // apply the running compensation
18 const t = sum + y
19 c = (t - sum) - y // recover what rounding discarded
20 sum = t
21 }
22 return sum
23}
24// kahanSum(values) === 2

Key points

  • A fraction is exact in binary only when its denominator is a power of two, so 0.1, 0.2 and 0.3 never were.
  • The inputs were rounded before the operation; the arithmetic itself was performed correctly.
  • Absolute tolerances fail because representable spacing varies with magnitude — use a relative tolerance with an absolute floor.
  • Adding a small value to a much larger accumulator can change nothing at all.
  • Floating-point addition is not associative, so parallel reductions can legitimately differ from sequential ones and from each other.

Progressive depth

Overview

0.1 has no exact binary representation, so the value stored was never 0.1. The addition was performed correctly on two approximations and the result rounded again.

Practical

Never compare computed floats for equality; use a relative tolerance with an absolute floor. Never store money in binary floating point. When summing many values, be aware that order matters and that small values can vanish into a large accumulator.

Advanced

Error accumulates roughly with the square root of the operation count for random rounding, but adversarially it can be far worse — catastrophic cancellation, where subtracting two nearly equal values annihilates the significant digits and promotes accumulated noise into the leading position, is the classic failure. Compensated summation recovers most of the lost precision by tracking the discarded low-order bits explicitly.

Internals

Addition aligns exponents by right-shifting the smaller operand's significand, which is where bits are physically lost before the add even happens. Hardware keeps guard, round and sticky bits beyond the stored width to make the final rounding correct, and a fused multiply-add computes a * b + c with a single rounding instead of two — which is why enabling FMA contraction changes results even though it is strictly more accurate. Subnormal values near zero trade precision for reach, and on some hardware are handled at reduced throughput, so a computation drifting into that range can slow down measurably.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Literal → rounding: the decimal in source is rounded to the nearest representable binary value at compile or parse time.
  2. 2
    Operands → FPU: the two stored approximations are added, with exponents aligned so the smaller value is shifted right.
  3. 3
    Alignment → bit loss: shifting the smaller operand right can push its significant bits past the available width, discarding them.
  4. 4
    Exact sum → rounding: the exact result is rounded again to the nearest representable value.
  5. 5
    Accumulated error → comparison: repeated rounding leaves a result that differs from the mathematical answer by an amount that grows with the number of operations.
What people conclude from this — wrongly
  • Assuming the language or the display is at fault; the stored values genuinely differ, as printing them at full precision shows.
  • Believing a wider type eliminates the problem rather than reducing its rate.
  • Choosing an absolute epsilon without reference to the magnitudes being compared.
  • Treating a differing parallel-reduction result as a race condition when it is order-dependent rounding.

Consequences, controls and cost

What it causes
  • • Equality comparison between computed floating-point values is unreliable and should generally not be used.
  • • Summation order changes the result, so parallel and sequential reductions disagree, and runs can differ from one another.
  • • Accumulating many small values into a large total silently loses a portion of them.
  • • Financial calculations in binary floating point drift from the exact decimal answer in ways audits will notice.
What you can do
  • • Do not use binary floating point for money or any quantity requiring exactness at a fixed decimal scale — use integer minor units or a decimal type.
  • • Compare with a relative tolerance plus an absolute floor, and choose the tolerance from the expected error of the computation.
  • • Use compensated summation or a wider accumulator when summing many values, especially of differing magnitudes.
  • • Sort or group operands so that values of similar magnitude are combined first.
How to see it
  • • Print operands and results at full precision — around 17 significant digits for a double — rather than at default precision.
  • • Sum the same data in several orders; the spread between results estimates the accumulated error directly.
  • • Recompute a critical result in higher precision and compare, to bound how much error the working precision introduced.
  • • Assert that no NaN or infinity has entered a computation at its boundaries, where the cause is still identifiable.
What it costs
  • • Compensated summation roughly doubles the arithmetic in the loop and inhibits some vectorisation, for accuracy most workloads do not need.
  • • Decimal types are exact at a fixed scale but substantially slower and generally unsupported by vector units.
  • • Sorting before summation costs more than the summation itself unless the data is already ordered.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • GENERALIEEE-754 rounding is specified precisely, so 0.1 + 0.2 produces the same bits on every conforming implementation. This is portable behaviour, not a quirk of one platform.
  • PLATFORM-SPECIFICCompilers may contract a multiply and add into a fused multiply-add with a single rounding, or keep intermediates in wider registers. Both change results slightly and are controlled by optimisation flags, so identical source can differ across builds.

Misconceptions

Claim
“0.1 + 0.2 !== 0.3 is a bug in the language.”
Reality
Every conforming implementation produces the same result, because none of the three values is exactly representable in binary. It is a property of the format, and it is specified rather than accidental.
Claim
“Rounding the result to a few decimal places fixes it.”
Reality
It fixes the display. The stored value is still an approximation, and rounding at each step introduces its own error, which can be worse than leaving the value alone until the end.
Claim
“A parallel sum returning a different answer indicates a data race.”
Reality
A correct parallel reduction regroups the additions, and since floating-point addition is not associative, a different grouping legitimately produces a different result. Reproducibility requires fixing the reduction order.