Почему 0.1 + 0.2 ≠ 0.3: плавающая точка
Объяснение, почему арифметика с плавающей запятой даёт неточные результаты вроде 0.1 + 0.2 = 0.30000000000000004. Стандарт IEEE 754, двоичное представление чисел с плавающей точкой и способы избежать ошибок в Python, JS.
Почему арифметика с плавающей запятой даёт кажущиеся неверные результаты?
Рассмотрим следующие примеры кода:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Почему эти математические неточности возникают при вычислениях с плавающей запятой?
Арифметика с плавающей запятой даёт кажущиеся неверные результаты вроде 0.1 + 0.2 == 0.3 (false) или 0.1 + 0.2 (0.30000000000000004), потому что компьютеры хранят числа с плавающей точкой в двоичной системе по стандарту IEEE 754. Многие десятичные дроби, такие как 0.1 или 0.2, не имеют точного конечного представления в двоичном виде — они аппроксимируются, и при операциях эти погрешности накапливаются. Это не баг языка вроде Python или JavaScript, а фундаментальное ограничение двоичной арифметики.
Содержание
- Числа с плавающей точкой: почему возникают неточности
- Классический пример: 0.1 + 0.2 ≠ 0.3
- Стандарт IEEE 754: как устроено хранение
- Двоичное представление 0.1 и 0.2
- Что происходит при сложении и округлении
- Практические способы избежать проблем
- Источники
- Заключение
Числа с плавающей точкой: почему возникают неточности
Представьте: вы пишете простой код, ожидая точного результата, а получаете странную цифру в конце. Почему числа с плавающей точкой в Python, JavaScript или Excel ведут себя так? Всё дело в том, как компьютер представляет вещественные числа. В отличие от целых, где всё чётко, дроби хранятся в формате с плавающей запятой — это двоичная аналогия научной записи вроде .
Большинство десятичных дробей вроде 0.1 (что равно ) не укладываются в конечную последовательность двоичных разрядов. Двоичная система работает с делителями на 2: 0.5, 0.25, 0.125 и так далее. А 0.1? Это бесконечная дробь в двоичном виде, подобно тому как в десятичной. Компьютер обрезает её по фиксированному числу бит — обычно 23 для одинарной точности (float) или 52 для двойной (double). Отсюда и погрешность.
Согласно документации Microsoft, это не ошибка Excel или любого другого ПО: “Большинство значений с плавающей запятой нельзя точно представить как конечное двоичное значение”. То же в Python-документации: 0.1 + 0.2 слегка больше 0.3 из-за приближений.
Классический пример: 0.1 + 0.2 ≠ 0.3
Запустите в консоли Python или браузере:
print(0.1 + 0.2 == 0.3) # False
print(0.1 + 0.2) # 0.30000000000000004
Или в JavaScript:
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2); // 0.30000000000000004
Почему именно такая цифра? Не случайность. Это следствие округления в последнем бите мантиссы. Выглядит как ошибка округления, но на деле — норма для арифметики с плавающей запятой. Как объясняют на Habr, после исследований автор пришёл к выводу: “Это не ошибка. Это математика”.
А вы пробовали умножить? 0.1 * 0.2 тоже не 0.02 ровно. Такие сюрпризы поджидают в финансах, графике, машинном обучении. Но есть ли способ разобраться глубже?
Стандарт IEEE 754: как устроено хранение
Почти все современные процессоры и языки используют стандарт IEEE 754 для чисел с плавающей запятой. Число кодируется 32 или 64 битами:
- Знак (s): 1 бит (0 — положительное).
- Экспонента (e): 8 бит для single (смещение 127), 11 для double (1023).
- Мантисса (m): 23/52 бита дробной части (скрытая 1-я единица).
Из NEERC Wiki: мантисса ограничена битами, экспонента — нормализацией. Это даёт диапазон от крошечных до огромных чисел, но с фиксированной относительной точностью (~15 десятичных цифр для double).
Конвертер IEEE 754 покажет: введите 0.1 — увидите двоичные биты, где мантисса обрезана.
Двоичное представление 0.1 и 0.2
Разложим 0.1 в двоичное: (бесконечно). Аналогично 0.2: . Компьютер берёт первые 52 бита и округляет.
На Stack Overflow объясняют: числа с плавающей точкой — это бинарные фракции (1/2, 1/4…), а не десятичные (1/10). Десятичные дроби аппроксимируются, как 1/3 в десятичной.
В блоге struchkov.dev детально: для 0.1 мантисса — 1.1001100110011001100110011001100110011001100110011010 (с скрытой 1), экспонента скорректирована. Сумма таких приближений выходит за 0.3 ровно.
Проверьте сами: в Python format(0.1, '.50f') покажет длинную дробь.
Что происходит при сложении и округлении
Алгоритм сложения в IEEE 754:
- Выравнивание экспонент (сдвиг мантиссы меньшей).
- Сложение мантисс.
- Нормализация и округление (к ближайшему, с учётом ULP — unit in last place).
Для 0.1 + 0.2 экспоненты совпадают, мантиссы складываются с переполнением в последнем бите — отсюда +0.00000000000000004. Как в Medium-статье: разница в один ULP.
Ошибки накапливаются: в цепочке вычислений погрешность растёт. В Reddit-обсуждении упрощённо: ограничено ~15 значащими цифрами.
Практические способы избежать проблем
Не паникуйте — есть фиксы.
- Сравнение с допуском (epsilon):
EPS = 1e-9
if abs(0.1 + 0.2 - 0.3) < EPS: # True
-
Округление:
round(0.1 + 0.2, 1) == 0.3 -
Точные типы: В Python —
decimal.Decimal:
from decimal import Decimal
Decimal('0.1') + Decimal('0.2') == Decimal('0.3') # True
- В JS — BigDecimal или библиотеки вроде decimal.js.
Для финансов — всегда Decimal. В графике — работайте с относительными ошибками. Подробнее в JS-блоге.
Источники
- Арифметические операции с плавающей запятой могут давать неточный результат в Excel
- Число с плавающей запятой — Википедия
- Проблемы точности чисел float в Python
- Is floating-point math broken? — Stack Overflow
- Как получилось, что 0,1 + 0,2 = 0,30000000000000004? — Habr
- Как получилось, что 0,1 + 0,2 не равно 0,3
- IEEE-754 Floating Point Converter
- Представление вещественных чисел — NEERC Wiki
Заключение
Арифметика с плавающей точкой кажется неверной из-за двоичного представления чисел по IEEE 754 — 0.1 и 0.2 аппроксимируются, их сумма выходит чуть больше 0.3. Главный takeaway: не сравнивайте напрямую, используйте epsilon или Decimal для точности. Теперь, зная механику мантиссы и округления, вы избежите ловушек в коде. Пробуйте конвертеры — увидите магию бит сами.