Программирование

Почему 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, а фундаментальное ограничение двоичной арифметики.


Содержание


Числа с плавающей точкой: почему возникают неточности

Представьте: вы пишете простой код, ожидая точного результата, а получаете странную цифру в конце. Почему числа с плавающей точкой в Python, JavaScript или Excel ведут себя так? Всё дело в том, как компьютер представляет вещественные числа. В отличие от целых, где всё чётко, дроби хранятся в формате с плавающей запятой — это двоичная аналогия научной записи вроде 1.23×1041.23 \times 10^4.

Большинство десятичных дробей вроде 0.1 (что равно 1/101/10) не укладываются в конечную последовательность двоичных разрядов. Двоичная система работает с делителями на 2: 0.5, 0.25, 0.125 и так далее. А 0.1? Это бесконечная дробь в двоичном виде, подобно тому как 1/3=0.3331/3 = 0.333\ldots в десятичной. Компьютер обрезает её по фиксированному числу бит — обычно 23 для одинарной точности (float) или 52 для двойной (double). Отсюда и погрешность.

Согласно документации Microsoft, это не ошибка Excel или любого другого ПО: “Большинство значений с плавающей запятой нельзя точно представить как конечное двоичное значение”. То же в Python-документации: 0.1 + 0.2 слегка больше 0.3 из-за приближений.


Классический пример: 0.1 + 0.2 ≠ 0.3

Запустите в консоли Python или браузере:

python
print(0.1 + 0.2 == 0.3) # False
print(0.1 + 0.2) # 0.30000000000000004

Или в JavaScript:

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 битами:

(1)s×(1+m)×2ebias(-1)^s \times (1 + m) \times 2^{e - bias}

  • Знак (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.110=0.000110011001120.1_{10} = 0.0001100110011\ldots_2 (бесконечно). Аналогично 0.2: 0.00110011001120.001100110011\ldots_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:

  1. Выравнивание экспонент (сдвиг мантиссы меньшей).
  2. Сложение мантисс.
  3. Нормализация и округление (к ближайшему, с учётом ULP — unit in last place).

Для 0.1 + 0.2 экспоненты совпадают, мантиссы складываются с переполнением в последнем бите — отсюда +0.00000000000000004. Как в Medium-статье: разница в один ULP.

Ошибки накапливаются: в цепочке вычислений погрешность растёт. В Reddit-обсуждении упрощённо: ограничено ~15 значащими цифрами.


Практические способы избежать проблем

Не паникуйте — есть фиксы.

  1. Сравнение с допуском (epsilon):
python
EPS = 1e-9
if abs(0.1 + 0.2 - 0.3) < EPS: # True
  1. Округление: round(0.1 + 0.2, 1) == 0.3

  2. Точные типы: В Python — decimal.Decimal:

python
from decimal import Decimal
Decimal('0.1') + Decimal('0.2') == Decimal('0.3') # True
  1. В JS — BigDecimal или библиотеки вроде decimal.js.

Для финансов — всегда Decimal. В графике — работайте с относительными ошибками. Подробнее в JS-блоге.


Источники

  1. Арифметические операции с плавающей запятой могут давать неточный результат в Excel
  2. Число с плавающей запятой — Википедия
  3. Проблемы точности чисел float в Python
  4. Is floating-point math broken? — Stack Overflow
  5. Как получилось, что 0,1 + 0,2 = 0,30000000000000004? — Habr
  6. Как получилось, что 0,1 + 0,2 не равно 0,3
  7. IEEE-754 Floating Point Converter
  8. Представление вещественных чисел — NEERC Wiki

Заключение

Арифметика с плавающей точкой кажется неверной из-за двоичного представления чисел по IEEE 754 — 0.1 и 0.2 аппроксимируются, их сумма выходит чуть больше 0.3. Главный takeaway: не сравнивайте напрямую, используйте epsilon или Decimal для точности. Теперь, зная механику мантиссы и округления, вы избежите ловушек в коде. Пробуйте конвертеры — увидите магию бит сами.

Авторы
Проверено модерацией
Модерация
Почему 0.1 + 0.2 ≠ 0.3: плавающая точка