Floating-point numbers and machine epsilon
Understand IEEE-754 floating point, machine epsilon, and where rounding error comes from.
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 , an exponent , and a fixed-precision mantissa . The value is
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 , , and NaN (not-a-number, the result of or ).
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 and , the spacing is where is the mantissa bit count. Near 1, the gap is tiny ( for float64); near , it is huge.
Machine epsilon
Machine epsilon is the gap between 1 and the next representable float above it:
For float64, ; for float32, . Any real that lies between two consecutive floats is rounded to one of them, with relative error bounded by
So floating point is accurate to about 16 decimal digits (double) or 7 (single), relatively, not absolutely. A number of magnitude has absolute uncertainty of order 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 [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 . Computing 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.
Check it on arrays
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 has no finite binary expansion, so it is stored as the nearest float64. Using the relative-error bound with , bound the absolute error of , and explain why is not exactly .
Solution
Step 1. The relative error bound gives an absolute error . So is stored with an error of order , not zero.
Step 2. The same holds for and : each is rounded to a nearby float. The stored values of and are each slightly off, and their sum is rounded again. There is no reason the rounded sum lands on the float nearest to , and it does not: evaluates to , which differs from 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 is representable in float64 but is not, and that rounds to . Use that the gap between consecutive floats in is with .
Solution
Step 1. At magnitude , the exponent is , so the spacing to the next float is . Consecutive integers near are exactly representable, so lands on a float.
Step 2. At magnitude , the exponent is , so the spacing is . The representable floats near are and ; the odd integer sits halfway between them.
Step 3. Round-to-nearest with the ties-to-even rule picks the even neighbor. has its last mantissa bit zero (even), does not at that scale, so rounds down to . The integer is lost: float64 stops representing every integer exactly past .
Exercise 3 (code). Floating-point addition is not associative. Construct three float64 values for which , and confirm the two groupings differ by exactly . Explain the mechanism in one sentence.
Solution
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 rounds back to because the
spacing of floats near exceeds , so the is swallowed before
cancels ; 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]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]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]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).↩