AI Knowledge Tree

Floating-point numbers and machine epsilon

Understand IEEE-754 floating point, machine epsilon, and where rounding error comes from.

⏱ 28 min

Every real number a computer manipulates is an approximation. Understanding which approximation, and how big the error is, is what separates code that quietly produces wrong answers from code that does not. Floating point follows a strict standard (IEEE-754) with an exactly characterized error floor: machine epsilon[1, 2].

The representation

A floating-point number stores three pieces: a sign s∈{0,1}s \in \{0, 1\}, an exponent ee, and a fixed-precision mantissa mm. The value is

x=(−1)s⋅(1.m1m2…mp)2⋅2e.x = (-1)^s \cdot (1.m_1 m_2 \dots m_p)_2 \cdot 2^{e}.

The leading 1 is implicit (a normalized binary number starts with 1), so a 24-bit mantissa actually stores 23 explicit bits. IEEE-754 single precision (float32) uses 8 bits of exponent and 23 of mantissa for a total of 32 bits; double precision (float64) uses 11 and 52 for 64 bits. There are also special encodings for ±0\pm 0, ±∞\pm \infty, and NaN (not-a-number, the result of 0/00/0 or ∞−∞\infty - \infty).

Representable numbers and gaps

Representable floats are not evenly spaced on the real line: they are evenly spaced between consecutive powers of two and then double. Between 2k2^k and 2k+12^{k+1}, the spacing is 2k−p2^{k-p} where pp is the mantissa bit count. Near 1, the gap is tiny (≈10−16\approx 10^{-16} for float64); near 102010^{20}, it is huge.

Machine epsilon

Machine epsilon is the gap between 1 and the next representable float above it:

εmachine=2−p.\varepsilon_{\text{machine}} = 2^{-p}.

For float64, εmachine≈2.22×10−16\varepsilon_{\text{machine}} \approx 2.22 \times 10^{-16}; for float32, ≈1.19×10−7\approx 1.19 \times 10^{-7}. Any real xx that lies between two consecutive floats is rounded to one of them, with relative error bounded by

∣fl(x)−x∣∣x∣≤εmachine2.\frac{|\text{fl}(x) - x|}{|x|} \le \frac{\varepsilon_{\text{machine}}}{2}.

So floating point is accurate to about 16 decimal digits (double) or 7 (single), relatively, not absolutely. A number of magnitude 102010^{20} has absolute uncertainty of order 10410^4 even in double precision.

Where error enters

A single arithmetic operation in IEEE-754 returns the exact result rounded to the nearest representable float. Each +, *, / introduces relative error of size at most εmachine/2\varepsilon_{\text{machine}}/2[3]. Algorithms with many operations can accumulate these errors; the next lesson on conditioning and stability formalizes how much.

Two specific phenomena dominate practical problems:

  • Overflow: the result exceeds the largest representable float and becomes ±∞\pm\infty. Computing e1000e^{1000} overflows in any double-precision system. The log-sum-exp trick (later lesson) exists to dodge this in softmax.
  • Underflow: the result is smaller than the smallest normal float and rounds to zero, losing all information. Multiplying many probabilities is the classic example, which is why likelihoods are computed in log space.
Floating-point values are spaced logarithmically: the gap to the next representable number (one ULP) grows with magnitude, so large numbers lose absolute precision.
Floating-point values are spaced logarithmically: the gap to the next representable number (one ULP) grows with magnitude, so large numbers lose absolute precision.

Check it on arrays

python · numpy
import numpy as np

# Machine epsilon
print("float64 eps:", np.finfo(np.float64).eps)        # ~2.22e-16
print("float32 eps:", np.finfo(np.float32).eps)        # ~1.19e-07

# 1 + eps/2 still rounds to 1 (it's below the spacing at 1)
print("1 + eps/2 == 1:", 1.0 + np.finfo(float).eps / 2 == 1.0)
# 1 + eps is the next representable number above 1
print("1 + eps > 1:", 1.0 + np.finfo(float).eps > 1.0)

# Spacing doubles with the exponent
for k in [0, 10, 20, 30]:
    x = 2.0**k
    print(f"spacing at 2^{k}:", np.spacing(x))

# Overflow and underflow in float32
inf = np.exp(np.float32(100))                          # overflows
zero = np.exp(np.float32(-100))                        # underflows
print("exp(100) f32:", inf, "  exp(-100) f32:", zero)

# Associativity fails in floating point: (a+b)+c != a+(b+c) in general
a, b, c = 1e16, -1e16, 1.0
print("(a+b)+c:", (a + b) + c, "  a+(b+c):", a + (b + c))

Machine epsilon defines the relative precision floor, the spacing doubles every power of two, overflow and underflow happen at predictable thresholds, and addition is not even associative.

Exercises

Exercise 1. Decimal 0.10.1 has no finite binary expansion, so it is stored as the nearest float64. Using the relative-error bound ∣fl(x)−x∣∣x∣≤εmachine2\frac{|\text{fl}(x) - x|}{|x|} \le \frac{\varepsilon_{\text{machine}}}{2} with εmachine≈2.22×10−16\varepsilon_{\text{machine}} \approx 2.22 \times 10^{-16}, bound the absolute error of fl(0.1)\text{fl}(0.1), and explain why 0.1+0.20.1 + 0.2 is not exactly 0.30.3.

Solution

Step 1. The relative error bound gives an absolute error ∣fl(0.1)−0.1∣≤εmachine2⋅0.1≈1.11×10−17|\text{fl}(0.1) - 0.1| \le \frac{\varepsilon_{\text{machine}}}{2} \cdot 0.1 \approx 1.11 \times 10^{-17}. So 0.10.1 is stored with an error of order 10−1710^{-17}, not zero.

Step 2. The same holds for 0.20.2 and 0.30.3: each is rounded to a nearby float. The stored values of fl(0.1)\text{fl}(0.1) and fl(0.2)\text{fl}(0.2) are each slightly off, and their sum is rounded again. There is no reason the rounded sum lands on the float nearest to 0.30.3, and it does not: 0.1+0.20.1 + 0.2 evaluates to 0.300000000000000040.30000000000000004, which differs from fl(0.3)=0.3\text{fl}(0.3) = 0.3 in the last bit. Each operand carries its own rounding error and the addition rounds the result a third time, so the three errors do not cancel.

Exercise 2. Show that 252+12^{52} + 1 is representable in float64 but 253+12^{53} + 1 is not, and that 253+12^{53} + 1 rounds to 2532^{53}. Use that the gap between consecutive floats in [2k,2k+1)[2^k, 2^{k+1}) is 2k−p2^{k - p} with p=52p = 52.

Solution

Step 1. At magnitude 2522^{52}, the exponent is k=52k = 52, so the spacing to the next float is 2k−p=252−52=20=12^{k - p} = 2^{52 - 52} = 2^0 = 1. Consecutive integers near 2522^{52} are exactly representable, so 252+12^{52} + 1 lands on a float.

Step 2. At magnitude 2532^{53}, the exponent is k=53k = 53, so the spacing is 253−52=21=22^{53 - 52} = 2^1 = 2. The representable floats near 2532^{53} are 2532^{53} and 253+22^{53} + 2; the odd integer 253+12^{53} + 1 sits halfway between them.

Step 3. Round-to-nearest with the ties-to-even rule picks the even neighbor. 2532^{53} has its last mantissa bit zero (even), 253+22^{53} + 2 does not at that scale, so 253+12^{53} + 1 rounds down to 2532^{53}. The integer +1+1 is lost: float64 stops representing every integer exactly past 2532^{53}.

Exercise 3 (code). Floating-point addition is not associative. Construct three float64 values a,b,ca, b, c for which (a+b)+c≠a+(b+c)(a + b) + c \neq a + (b + c), and confirm the two groupings differ by exactly 11. Explain the mechanism in one sentence.

Solution
python · numpy
import numpy as np

a, b, c = 1e16, -1e16, 1.0
left  = (a + b) + c     # (0.0) + 1.0 = 1.0
right = a + (b + c)     # 1e16 + (-1e16 + 1.0); the +1 is below the gap at 1e16
assert left == 1.0
assert right == 0.0
assert left - right == 1.0
print("(a+b)+c =", left, "  a+(b+c) =", right)

In a + (b + c), the inner sum −1016+1-10^{16} + 1 rounds back to −1016-10^{16} because the spacing of floats near 101610^{16} exceeds 11, so the +1+1 is swallowed before aa cancels bb; grouping the cancellation first preserves it.

With this representation in hand, the next lesson asks: when does rounding error matter, and when does it explode?


References

  1. [1]IEEE. (2019). IEEE Standard for Floating-Point Arithmetic (IEEE 754-2019). The normative definition of the formats, rounding, and special values (doi:10.1109/IEEESTD.2019.8766229).↩
  2. [2]Goldberg, D. (1991). "What every computer scientist should know about floating-point arithmetic". ACM Computing Surveys, 23(1), 5-48 (doi:10.1145/103162.103163).↩
  3. [3]Higham, N. J. (2002). Accuracy and Stability of Numerical Algorithms (2nd ed.). SIAM. The standard-model rounding bound on each operation (doi:10.1137/1.9780898718027).↩