Машинный эпсилон и почему 0.1 + 0.2 ≠ 0.3
Урок про машинный эпсилон — фундаментальную «зернистость» вещественной арифметики — и про знаменитое 0.1 + 0.2 ≠ 0.3.
Машинный эпсилон (eps) — наименьшее положительное число такое, что
1.0 + eps ≠ 1.0в машинной арифметике. Для double он равен2^-52 ≈ 2.22 × 10^-16.
Что такое машинный эпсилон
Из-за конечной мантиссы существует порог: если к единице прибавить слишком маленькое число, оно просто «не дотянется» до следующего представимого значения и пропадёт. Этот порог и есть машинный эпсилон. Он характеризует относительную точность арифметики: любую величину компьютер хранит с относительной ошибкой не больше eps/2.
Найти его легко: будем делить пробное значение пополам, пока 1 + проба ещё отличается от 1, и остановимся, когда перестанет.
# Ищем машинный эпсилон «вручную»
eps = 1.0
while 1.0 + eps / 2.0 != 1.0:
eps /= 2.0
print("машинный эпсилон:", eps)
print("это 2^-52? ", eps == 2.0 ** -52)
# То же значение лежит в стандартной библиотеке
import sys
print("sys.float_info.epsilon:", sys.float_info.epsilon)
Вывод:
машинный эпсилон: 2.220446049250313e-16 это 2^-52? True sys.float_info.epsilon: 2.220446049250313e-16
Знаменитое 0.1 + 0.2
Теперь разгадка классики. 0.1 хранится как чуть-чуть больше 0.1, 0.2 — как чуть-чуть больше 0.2, их сумма округляется к представимому числу, которое оказывается чуть больше истинного 0.3. А 0.3, записанное напрямую, округляется к другому, чуть меньшему представимому числу. Эти два «чуть» не совпадают — отсюда неравенство.
a = 0.1 + 0.2
print("0.1 + 0.2 =", a)
print("равно 0.3? ", a == 0.3)
print("разница: ", a - 0.3)
# Правильное сравнение — через допуск
import math
print("isclose? ", math.isclose(a, 0.3))
print("вручную: ", abs(a - 0.3) < 1e-9)
Вывод:
0.1 + 0.2 = 0.30000000000000004 равно 0.3? False разница: 5.551115123125783e-17 isclose? True вручную: True
Разница 5.5e-17 — порядка машинного эпсилона. Это не баг Python и не баг процессора, это фундаментальное свойство двоичной плавающей точки; так же ведут себя C, Java, JavaScript и любой другой язык на IEEE 754.
Как работает под капотом
Машинный эпсилон задаёт «зернистость» вокруг 1.0. Вокруг другого числа x зернистость масштабируется: соседние представимые отстоят примерно на |x| · eps. Поэтому относительная ошибка хранения и одной операции ограничена eps/2. Когда вы видите в результате «хвост» вроде ...00004 — это последние биты мантиссы, и доверять им нельзя. Практическое следствие: сравнивайте вещественные числа не на точное равенство, а с допуском, согласованным с масштабом величин.
1.0 1.0 + eps 1.0 + 2*eps
|---------------|---------------|---------->
<----- eps ----->
числа между соседними отметками НЕ представимы: они округляются
к ближайшей отметке. eps = ширина одного «зерна» возле единицы.
Частые ошибки
- Накапливать сумму многих мелких чисел наивно. Складывая
0.1миллион раз, получите заметное отклонение от100000. Помогает сложение Кэхэна или суммирование от меньших к большим. - Брать слишком жёсткий допуск. Сравнение
abs(a−b) < 1e-18бессмысленно: это меньше eps, такой допуск никогда не сработает для чисел порядка 1. - Брать абсолютный допуск для величин разного масштаба. Для чисел порядка
10^9допуск1e-9абсурдно строг — нужен относительный (как вmath.isclose).
Итоги
- Машинный эпсилон =
2^-52 ≈ 2.22e-16— наименьшая добавка, ещё меняющая 1.0. - Он задаёт относительную точность: ошибка хранения/операции ≤
eps/2. 0.1 + 0.2 ≠ 0.3— не баг, а следствие двоичного представления десятичных дробей.- Сравнивать float нужно с допуском (
math.isclose), согласованным с масштабом чисел.