Машинный эпсилон и почему 0.1 + 0.2 ≠ 0.3

Урок про машинный эпсилон — фундаментальную «зернистость» вещественной арифметики — и про знаменитое $0.1 + 0.2 \ne 0.3$.

Машинный эпсилон ($\varepsilon$) — наименьшее положительное число такое, что $1.0 + \varepsilon \ne 1.0$ в машинной арифметике. Для double он равен $2^{-52} \approx 2.22 \times 10^{-16}$.

Что такое машинный эпсилон

Из-за конечной мантиссы существует порог: если к единице прибавить слишком маленькое число, оно просто «не дотянется» до следующего представимого значения и пропадёт. Этот порог и есть машинный эпсилон. Он характеризует относительную точность арифметики: любую величину компьютер хранит с относительной ошибкой не больше $\varepsilon/2$.

Найти его легко: будем делить пробное значение пополам, пока $1 + \text{проба}$ ещё отличается от 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.5 \times 10^{-17}$ — порядка машинного эпсилона. Это не баг Python и не баг процессора, это фундаментальное свойство двоичной плавающей точки; так же ведут себя C, Java, JavaScript и любой другой язык на IEEE 754.

Как работает под капотом

Машинный эпсилон задаёт «зернистость» вокруг 1.0. Вокруг другого числа $x$ зернистость масштабируется: соседние представимые отстоят примерно на $|x| \cdot \varepsilon$. Поэтому относительная ошибка хранения и одной операции ограничена $\varepsilon/2$. Когда вы видите в результате «хвост» вроде ...00004 — это последние биты мантиссы, и доверять им нельзя. Практическое следствие: сравнивайте вещественные числа не на точное равенство, а с допуском, согласованным с масштабом величин.

  1.0          1.0 + eps      1.0 + 2*eps
   |---------------|---------------|---------->
   <----- eps ----->
  числа между соседними отметками НЕ представимы: они округляются
  к ближайшей отметке. eps = ширина одного «зерна» возле единицы.

Частые ошибки

  • Накапливать сумму многих мелких чисел наивно. Складывая $0.1$ миллион раз, получите заметное отклонение от $100000$. Помогает сложение Кэхэна или суммирование от меньших к большим.
  • Брать слишком жёсткий допуск. Сравнение $|a - b| \lt 10^{-18}$ бессмысленно: это меньше $\varepsilon$, такой допуск никогда не сработает для чисел порядка 1.
  • Брать абсолютный допуск для величин разного масштаба. Для чисел порядка $10^9$ допуск $10^{-9}$ абсурдно строг — нужен относительный (как в math.isclose).

Итоги

  • Машинный эпсилон = $2^{-52} \approx 2.22 \times 10^{-16}$ — наименьшая добавка, ещё меняющая 1.0.
  • Он задаёт относительную точность: ошибка хранения/операции $\le \varepsilon/2$.
  • $0.1 + 0.2 \ne 0.3$ — не баг, а следствие двоичного представления десятичных дробей.
  • Сравнивать float нужно с допуском (math.isclose), согласованным с масштабом чисел.
Проверьте себя
1. Чему равен машинный эпсилон для IEEE 754 double?
A2^-23 ≈ 1.2e-7
B2^-52 ≈ 2.22e-16
C2^-64 ≈ 5.4e-20
D10^-16 ровно
2. Что показывает условие 1.0 + eps/2 == 1.0 при поиске машинного эпсилона?
AЧто eps/2 уже меньше зернистости вокруг 1.0 и теряется при сложении
BЧто компьютер сломался
CЧто eps равно нулю
DЧто мантисса переполнилась
3. Как корректно сравнивать два float на «равенство»?
AЧерез ==
BЧерез допуск: abs(a−b) меньше выбранного eps или math.isclose
CОкруглив оба до int
DПереведя оба в строки
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →