skip to content

questions

5

Why does == fail on two 64-bit float path lengths that are algebraically equal?

level: juniorimportance: must knowfreq 88%

answer

  1. think about what base the hardware uses
  2. one tenth in binary, like one third in decimal
  3. each operation rounds its own result
  4. different orders, different accumulated error
  5. addition here is commutative but not associative

basics

~20 s

Binary 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 s

A 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

When does an absolute epsilon comparison of two 64-bit floats fail, and what replaces it?

level: middleimportance: must knowfreq 66%

basics

~20 s

A fixed absolute epsilon only works in a narrow magnitude band: near 1e12 adjacent 64-bit floats are further apart than 1e-9, while near zero the same epsilon calls wildly different values equal. Use a relative tolerance with an absolute floor.

open as a page

How do NaN and Infinity readings break a max-finding scan over a sensor stream?

level: middleimportance: should knowfreq 47%

basics

~20 s

Every comparison involving NaN is false, so a NaN seed pins the running maximum at NaN forever, while a NaN arriving later is silently skipped. Infinity compares as an ordinary value and wins every comparison. Neither is reported as an error.

open as a page

Why mandate decimal or fixed-point money in a billing ledger when floats hold 15 digits?

level: principalimportance: should knowfreq 52%

basics

~20 s

One hundredth is non-terminating in binary, so every amount held in a 64-bit float is approximate from the first assignment and error accumulates across millions of postings. Decimal or integer minor units make amounts exact and rounding an explicit, auditable rule.

open as a page

Why do 64-bit record identifiers get silently corrupted by a 64-bit-float numeric type?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

A 64-bit float carries a 53-bit significand, so it represents integers exactly only up to 2^53, about 9.0e15. Larger identifiers round to the nearest representable value with no error raised, so distinct IDs shift by one or collide with each other.

open as a page