Why does == fail on two 64-bit float path lengths that are algebraically equal?
answer
- think about what base the hardware uses
- one tenth in binary, like one third in decimal
- each operation rounds its own result
- different orders, different accumulated error
- addition here is commutative but not associative
basics
~20 sBinary floating-point cannot hold most decimal fractions exactly, so every operation rounds. Two totals summed in different orders accumulate different rounding error and end up a few bits apart even when the algebra says they match. Compare with a tolerance.
solid answer
~50 sA 64-bit float stores a sign, an exponent and a 53-bit significand, so it can only hold numbers of the form (53-bit integer) x 2^k exactly. One tenth is non-terminating in binary the way one third is non-terminating in decimal, so a literal like `0.1` is already the nearest neighbour rather than the value itself. Every arithmetic operation then rounds its exact result once more. Addition is commutative but **not associative**: `(a + b) + c` and `a + (b + c)` can round differently, so two path lengths built from the same segments in different traversal orders legitimately differ in the last bits. `==` asks whether the two bit patterns are identical, which is a much stronger question than "are these the same length". The fix is to compare against a deliberately chosen tolerance, applied through one shared comparator rather than an epsilon invented at each call site.
go deeper
Be ready to say that binary floating-point stores most decimal fractions approximately, so == on computed values is unreliable. The one-tenth-is-non-terminating-in-binary explanation is the whole junior expectation here.
Explain the mechanism: each operation rounds its exact result to the nearest representable value, so different operation orders accumulate different error. Show that addition is commutative but not associative, and that a wider format does not fix it.
Show how you review this in a diff: name the tolerance, justify its size against the data's range and the pipeline's rounding budget, and route every comparison through one shared helper instead of scattered epsilons.
Own the policy: which subsystems may use binary floats at all, what the house tolerance convention is, and how numeric tests assert results so a compiler or hardware change does not turn a green suite red.
## What is actually stored A 64-bit binary floating-point value is three fields: a sign bit, an 11-bit exponent, and 52 stored significand bits (53 effective, because a leading 1 is implied for normal values). The value it denotes is `+/- m x 2^e` where `m` is that 53-bit integer. Consequently the *only* numbers a 64-bit float can hold exactly are those expressible as a 53-bit integer scaled by a power of two. One tenth is not one of them. In binary, 0.1 is `0.0001100110011...` with `0011` repeating forever, for exactly the reason one third is `0.333...` forever in decimal: the denominator has a prime factor the base does not. The machine stops after 53 significant bits and stores the nearest representable neighbour, which for 0.1 is about `0.1000000000000000055511151231257827`. The same is true of 0.2, 0.3, and 0.01. Nothing has gone wrong yet: the value is off by roughly one part in 10^17 and is the best a finite representation can do. ## Why every operation adds a little more IEEE-754 arithmetic is *correctly rounded*: each individual operation computes the exact mathematical result of the operands it was given and then rounds that result once to the nearest representable value. So error enters in two places — once when a decimal literal is converted, and once per operation afterwards. Add the nearest float to 0.1 and the nearest float to 0.2 and the exact sum is not representable; the rounded sum is a value slightly above the nearest float to 0.3. Two approximations plus one rounding produce a result that is close to, but not identical with, the approximation you would have got by converting 0.3 directly. ## Why operation order changes the answer This is the part that bites in a geometry routine. Floating-point addition is commutative — `a + b` and `b + a` give bit-identical results — but it is **not associative**. Consider accumulating segment lengths of wildly different magnitudes. If the running total has already grown large, adding a very small segment may round it away entirely, because the exact sum falls between two representable neighbours and rounds back to the larger operand. Sum the small segments together first and they reinforce each other enough to survive. Same segments, same algebra, two different totals. Operation order is not the only source of variation. Compilers may contract a multiply followed by an add into a single fused operation with one rounding instead of two. Some hardware keeps wider intermediate precision in registers than the declared type. A vectorised reduction splits one sum into several lanes and combines them at the end, which is a different association. A trigonometric or square-root routine may be implemented differently in two libraries. None of these change the program's meaning, and every one of them can move the last bits. ## The reviewer's question So when a diff contains a comparison of two computed path lengths with `==`, the useful question is not "is one of these values wrong" — both are correct to within a rounding step — but "does this branch depend on a bit-for-bit accident that no one has written down?". A test that passes today because the two orders happened to round the same way will fail when someone reorders the loop, enables a fused multiply-add, or ships on different hardware. ## When exact equality is legitimate It is not always wrong. `==` is fine for a value you literally assigned and never did arithmetic to (a sentinel), for integers held in a float below 2^53 where in-range integer arithmetic is exact, and as an exact-zero guard before a division — with the caveat that zero has two signs, and a small non-zero product can round *to* zero. ## The remedy, in shape Compare with a tolerance chosen from the domain and from how many rounding steps the pipeline performs, and put that comparison in one reviewed place. Do not sprinkle epsilons at call sites, and do not use tolerance equality as the equality behind sorting, deduplication or keys — it is not transitive, so `a` can match `b` and `b` match `c` while `a` and `c` are far apart. When you need a key, quantise onto an explicit grid instead. ## Ecosystems differ in presentation, not in arithmetic Mainstream runtimes all execute the same binary64 arithmetic underneath, but they print it differently: some default to the shortest decimal string that round-trips back to the same value, so a stored 0.1 displays as "0.1" and the surprise only appears after arithmetic; others historically printed a fixed number of digits and showed the discrepancy sooner. That difference in default formatting is why the same expression looks unremarkable in one ecosystem and shocking in another, and it is a display convention only — the bits are identical.
- The two lengths came out bitwise identical on your machine. Is == safe to ship now?No. Bitwise agreement is an accident of this input, this traversal order, and the instructions the compiler chose. Reorder the loop, enable a fused multiply-add, vectorise the reduction, or run on hardware that keeps wider intermediates, and the same inputs land on a different last bit. A check that passes by coincidence is a check that will fail later. Compare against a documented tolerance so the contract is explicit rather than lucky.
- Is floating-point addition associative?No. `(a + b) + c` and `a + (b + c)` can differ, because each addition rounds its result and the intermediate magnitudes differ. Summing many small contributions after a large partial total can round the small ones away entirely, while summing them together first preserves them. Multiplication is likewise non-associative, and distributivity fails too. Only operations that happen to be exact — integer-valued arithmetic that stays inside the exact integer range — are order-independent.
- Would a wider floating-point format make exact equality safe?No, it moves the boundary rather than removing it. A wider significand shrinks each rounding error, but one tenth is still non-terminating in binary, so the identical class of mismatch reappears a few decimal digits further out. Widening buys headroom for accumulation, not exactness. Exact decimal fractions require a radix-10 or fixed-point representation; safe comparison of binary floats requires a tolerance either way.
One tenth in binary is like one third in decimal: you can write 0.333... forever and never land on it. The machine stops after 53 bits and keeps the nearest neighbour.
saying these in an interview costs you the question
- Claims the hardware is buggy or the stored value is wrong
- Says == is fine because it passed one test run
- Thinks enough extra precision eventually makes one tenth exact
- Blames the display formatting rather than the stored value
- Rounds to two decimals before == and calls it a general fix