Разработка ПО

Почему 0.1 + 0.2 ≠ 0.3 и при чём тут ваши деньги

Илья НиксанИлья Никсан
14 марта 2026 г.
Почему 0.1 + 0.2 ≠ 0.3 и при чём тут ваши деньги

14 марта — неофициальный праздник в мире программирования: день числа Пи (3.14). Это отличный повод поговорить о том, как компьютеры работают с дробными числами, почему результаты арифметических операций иногда выглядят неожиданно, и к каким последствиям это может привести в реальных проектах — особенно там, где речь идёт о деньгах.

Эксперимент, который удивляет каждого новичка

Откройте интерактивную консоль любого языка программирования и выполните простейшее выражение:

0.1 + 0.2

Dart, JavaScript, Python, Go, C++, Ruby — результат будет одинаковым: 0.30000000000000004. Не 0.3, как подсказывает здравый смысл, а число с длинным хвостом из нулей и четвёркой на конце.

Это не баг конкретного языка и не ошибка в вашем коде. Это фундаментальное свойство того, как современное оборудование представляет дробные числа в памяти.

Как компьютер хранит дробные числа

Вся информация в компьютере хранится в двоичном виде — последовательностях нулей и единиц. С целыми числами это работает безупречно: 5 в двоичной системе записывается как 101, 10 — как 1010. Каждое целое число имеет точное двоичное представление.

С дробными числами ситуация принципиально иная. Чтобы перевести десятичную дробь в двоичную, применяется метод последовательного умножения на 2 с выделением целой части. Посмотрим, что произойдёт с числом 0.1:

0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0  ← начало цикла
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1  ← конец цикла
0.2 × 2 = 0.4 → 0  ← цикл повторяется
...

В результате получается 0.0(0011) — бесконечная периодическая двоичная дробь. Ситуация аналогична тому, как в десятичной системе дробь 1/3 превращается в бесконечное 0.3333... — записать её точно в конечном числе знаков невозможно.

Число 0.2 сталкивается с той же проблемой. Оба числа, с которыми мы выполняем сложение, уже на этапе записи в память содержат погрешность.

Стандарт IEEE 754: компромисс между точностью и производительностью

Подавляющее большинство современных процессоров используют стандарт IEEE 754 для работы с числами с плавающей точкой. Тип double (64-битное число двойной точности), который является стандартным для большинства языков программирования, устроен следующим образом:

  • 1 бит отводится под знак числа (положительное или отрицательное)
  • 11 бит — под экспоненту (порядок числа)
  • 52 бита — под мантиссу (значащие цифры)

52 бита мантиссы — это примерно 15–17 значащих десятичных цифр. Когда двоичное представление дроби оказывается бесконечным, оно обрезается на 52-м бите. Именно в этот момент возникает погрешность представления.

При сложении двух чисел, каждое из которых уже содержит погрешность, ошибки накапливаются. Так 0.1 + 0.2 превращается в 0.30000000000000004.

Когда погрешность допустима

Справедливости ради, для большинства задач эта погрешность совершенно незначительна. Речь идёт об ошибке порядка 10⁻¹⁶ — это одна квадриллионная.

В следующих областях double работает прекрасно:

  • Компьютерная графика и рендеринг — разница в шестнадцатом знаке после запятой невидима для человеческого глаза
  • Физические симуляции в играх — объекты ведут себя реалистично, погрешность не влияет на восприятие
  • Научные вычисления — в большинстве случаев результат округляется до нужной точности
  • Статистика и машинное обучение — модели оперируют приближениями по своей природе

Когда погрешность становится критической: финансовые вычисления

Ситуация кардинально меняется, когда речь заходит о деньгах. В финансовых системах каждая копейка должна быть учтена точно. Ошибка в шестнадцатом знаке после запятой может показаться несущественной для одной транзакции, но при обработке тысяч и миллионов операций эти микроскопические погрешности накапливаются и превращаются в реальные расхождения в балансах.

Рассмотрим конкретный пример. Допустим, система обрабатывает 100 000 транзакций в день, и в каждой из них возникает погрешность в 0.000000000000004. За день это 0.0000000004 — ничтожно мало. Но умножьте на год, добавьте более сложные операции с умножением и делением, где погрешности растут значительно быстрее, — и вы получите суммы, которые невозможно объяснить ни бухгалтеру, ни аудитору.

Надёжные подходы к работе с денежными суммами

Подход первый: минорные единицы как целое число

Суть подхода проста: вместо хранения суммы в рублях или долларах как дробного числа, она хранится в наименьших единицах валюты (копейках, центах, пенни) как целое число.

19999 копеек — это всегда ровно 199 рублей и 99 копеек. Никаких округлений, никаких сюрпризов, никаких 0.000000004 в хвосте. Целочисленная арифметика в компьютерах абсолютно точна.

Этот подход широко используется в платёжных системах. Stripe, например, оперирует суммами исключительно в минимальных единицах валюты.

Отдельно стоит упомянуть альтернативный вариант для API: передача денежных сумм в виде строк. В этом случае интерфейс остаётся человекочитаемым ("199.99" понятнее, чем 19999), а ответственность за парсинг и выбор подходящего числового типа ложится на клиентскую сторону.

Подход второй: библиотеки произвольной точности (BigDecimal)

Второй надёжный вариант — использование специализированных типов данных, которые хранят десятичные числа без преобразования в двоичную систему:

  • Dart — пакет decimal
  • Javajava.math.BigDecimal
  • Python — модуль decimal из стандартной библиотеки
  • JavaScript — предложение Decimal на стадии рассмотрения, а пока — библиотеки вроде decimal.js
  • Go — пакет shopspring/decimal

Эти типы представляют числа в десятичном виде, полностью избегая двоичных приближений. Выражение 0.1 + 0.2 == 0.3 при использовании BigDecimal возвращает true — гарантированно.

Платой за точность является производительность: операции с BigDecimal значительно медленнее, чем с double. Однако в контексте финансовых вычислений, где корректность результата важнее скорости, это более чем приемлемый компромисс.

Какой подход выбрать

Оба подхода решают проблему, но подходят для разных ситуаций:

КритерийМинорные единицы (int)BigDecimal
ПроизводительностьМаксимальнаяНиже
Простота реализацииВысокаяСредняя (зависит от языка)
Поддержка дробных единицНет (только целые)Да
МультивалютностьТребует знания количества знаковРаботает из коробки

Для большинства e-commerce и платёжных систем подход с минорными единицами является оптимальным. BigDecimal предпочтительнее в сценариях, где необходимы промежуточные дробные вычисления — например, при расчёте процентов, налогов или конвертации валют.

Заключение

Если в вашем проекте есть денежные суммы, следует придерживаться одного из двух правил:

  • Хранить и передавать суммы в минорных единицах как целое число (int)
  • Использовать типы с произвольной точностью (BigDecimal и его аналоги)

Использование double для финансовых вычислений — это техническая задолженность, которая может не проявляться месяцами, но в определённый момент приведёт к расхождениям, причину которых будет крайне сложно диагностировать.

Выбирайте инструменты, соответствующие задаче, и будьте внимательны к тому, как ваш язык программирования представляет числа в памяти. Это одна из тех вещей, о которых полезно знать до того, как она станет проблемой на продакшене.

С днём числа Пи! 🥧