Получить доступ

Почему 0.1 + 0.2 не равно 0.3?

JuniorЧисла в компьютере
Ещё спрашивают так:
  • Как устроен тип double?
  • Что такое IEEE 754?
  • Как правильно сравнивать числа с плавающей точкой?
Коротко

double хранит число как двоичную дробь: знак, порядок и 52 бита мантиссы. 0.1 в двоичной системе бесконечна, как 1/3 в десятичной, — хранится приближение.

  • 0.1 + 0.2 = 0.30000000000000004: ошибки округления сложились
  • Сравнивать с допуском: |a − b| < ε, лучше относительным
  • Точно представимы целые до 2⁵³ и дроби вида k/2ⁿ

А ещё Спецзначения: NaN ≠ NaN, +0 и −0, бесконечности

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Попробуйте сами

Число изнутри: дополнительный код и плавающая точка

Подробно

Тип double (IEEE 754, 64 бита) хранит число в научной записи, только по основанию 2: знак (1 бит), порядок (11 бит, со смещением 1023) и мантисса (52 бита, плюс неявная ведущая единица). Значение — (−1)^знак × 1.мантисса × 2^(порядок − 1023).

Конечной двоичной дробью записываются только числа вида k/2ⁿ: 0.5, 0.25, 0.375. А 0.1 = 1/10, и в знаменателе есть пятёрка — в двоичной системе получается бесконечная периодическая дробь 0.000110011001100… Её обрезают до 53 значащих бит, и хранится ближайшее представимое число — 0.1000000000000000055511… То же с 0.2. При сложении ошибки складываются, результат округляется до ближайшего double, и это не тот double, который получается из литерала 0.3.

Сколько точности. 53 бита — это 15–17 значащих десятичных цифр. Все целые до 2⁵³ (≈ 9·10¹⁵) представимы точно, дальше — уже через одно. У float 24 бита мантиссы — около 7 цифр, и 16 777 217 в него не помещается.

Сравнение. == для вычисленных значений почти всегда ошибка. Сравнивают с допуском: абсолютным abs(a - b) < 1e-9 для чисел около единицы или относительным, как math.isclose, для чисел любого масштаба.

Спецзначения. +∞ и −∞ (1.0 / 0 в Java), NaN (0.0 / 0, корень из −1) — и NaN не равен ничему, даже себе, поэтому проверяют isnan. Есть −0.0, равный +0.0, но 1 / −0.0 = −∞.

Последствия: порядок сложения меняет результат (сумма большого числа малых теряет точность — помогает алгоритм Кэхэна или math.fsum), деньги в double не хранят, счётчик цикла с шагом 0.1 не дойдёт ровно до 1.0.

Пример

0.1 + 0.2                     # 0.30000000000000004
0.1 + 0.2 == 0.3              # False
import math
math.isclose(0.1 + 0.2, 0.3)  # True
from decimal import Decimal
Decimal(0.1)                  # 0.1000000000000000055511151231257827…
2.0 ** 53 + 1 == 2.0 ** 53    # True — дальше целые уже не все
float("nan") == float("nan")  # False

Что хотят услышать

  • Двоичное представление: 0.1 нельзя записать конечной двоичной дробью.
  • Устройство: знак, порядок, мантисса, примерно 15–17 цифр точности.
  • Сравнение с допуском, лучше относительным.
  • Следствие для денег — хранить не в double.
  • NaN и бесконечности — плюс.

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

  • «Компьютер считает неточно» без объяснения про двоичные дроби.
  • Сравнивать через == или с одним абсолютным ε для чисел любого масштаба.

Что спросят дальше

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем