Введение: Загадка «0.1 + 0.2»
Каждый программист рано или поздно сталкивается с этим парадоксом:
| |
Как современные суперкомпьютеры, моделирующие квантовые системы, могут ошибаться в сложении чисел уровня начальной школы? Это не ошибка в языке программирования. Любой современный процессор на Земле выдает строго одинаковый результат: 0.30000000000000004.
flowchart TD
subgraph Dec ["Десятичный мир (Интуиция человека)"]
D1["0.1 = 1/10 (Конечная дробь)"]
D2["0.2 = 1/5 (Конечная дробь)"]
D3["0.1 + 0.2 = 0.3 (Точно)"]
D1 --> D3
D2 --> D3
end
subgraph Bin ["Двоичный мир (Стандарт IEEE 754)"]
B1["0.1 = (0.000110011...)₂\nБесконечная периодическая дробь"]
B2["0.2 = (0.001100110...)₂\nБесконечная периодическая дробь"]
B3["Усечение до 53 бит и выравнивание порядков\nОкругление к ближайшему четному"]
B4["Значение в регистре:\n0.3000000000000000444..."]
B1 --> B3
B2 --> B3
B3 --> B4
end
Dec -.->|Цифровое представление| Bin
style Dec fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
style Bin fill:#ffe6e6,stroke:#cc0000,stroke-width:2px
1. Почему десятичные дроби становятся бесконечными в двоичном коде
В десятичной системе дробь $\frac{1}{3} = 0.3333\dots$ бесконечна. Несократимая дробь $\frac{P}{Q}$ конечна в системе счисления с основанием $N$ тогда и только тогда, когда все простые делители знаменателя $Q$ делят основание $N$.
- В десятичной системе ($10 = 2 \times 5$) делители 2 и 5 дают конечные дроби ($0.1 = \frac{1}{10}$).
- В двоичной системе единственный простой делитель — 2. Конечными являются лишь дроби со знаменателем $2^k$.
Поскольку разрядность регистров конечна, неизбежно происходит усечение числа.
2. Стандарт IEEE 754 и сложение в процессоре
По стандарту IEEE 754 (binary64) число кодируется в 64 битах:
- 1 бит знака
- 11 бит порядка (со смещением 1023)
- 52 бита мантиссы (53 бита эффективной точности благодаря неявному первому биту).
Поэтому проверка 0.1 + 0.2 === 0.3 возвращает false.
3. Исторические катастрофы и практические правила
- Трагедия с ракетой Patriot (1991): Накопление погрешности таймера на 0.1 с за 100 часов непрерывной работы привело к временному сдвигу на 0.34 с. Радар потерял ракету Scud на дистанции 687 метров, в результате чего погибло 28 солдат.
- Взрыв ракеты Ариан-5 (1996): Переполнение при конвертации 64-битного числа с плавающей запятой в 16-битное целое число привело к самоуничтожению ракеты стоимостью 500 миллионов долларов.
Правила для разработчиков:
- Никогда не сравнивайте числа с плавающей запятой через
==. Используйте машинный эпсилон $\epsilon$ (math.isclose). - В финансовых системах используйте целые числа (копейки/центы) или специализированные десятичные типы (
BigDecimal/decimal.Decimal).
5. Дополнительные исторические прецеденты: Баг процессора Pentium и Ванкуверская биржа
История информатики знает немало других драматических эпизодов, когда пренебрежение особенностями чисел с плавающей запятой приводило к колоссальным убыткам и технологическим кризисам.
5.1 Медленное крушение индекса Ванкуверской фондовой биржи (1982)
В 1982 году Ванкуверская фондовая биржа разработала новый биржевой индекс, стартовавший с отметки 1000.000. При каждой сделке индекс пересчитывался заново, однако в коде программы вместо честного округления к ближайшему числу использовалось простое усечение (отбрасывание) знаков после третьего десятичного разряда. Каждая операция отсекала крошечную положительную дробь. В результате тысяч ежедневных сделок этот систематический сдвиг вниз привел к тому, что за двадцать два месяца растущий в реальности рынок потерял по индексу почти половину величины, опустившись до 520 пунктов! Лишь после вмешательства математиков и исправления алгоритма на корректное округление истинный индекс мгновенно восстановился до отметки свыше 1098 пунктов.
5.2 Знаменитая ошибка FDIV в процессоре Intel Pentium (1994)
$$\frac{4195835}{3145727}$$процессор выдавал ошибочную мантиссу уже в пятом знаке из-за того, что на кристалле кремния в таблице алгоритма SRT по недосмотру инженеров оказались пустыми 5 ячеек из 2048. Скандал обошелся корпорации Intel в 475 миллионов долларов, потраченных на замену миллионов дефектных процессоров по всему миру.
6. Алгоритм компенсационного суммирования Кэхэна
Для устранения эффекта поглощения при суммировании миллионов чисел лауреат премии Тьюринга Уильям Кэхэн создал элегантный алгоритм:
| |
Этот алгоритм сводит накапливаемую погрешность практически к нулю даже при миллиардах итераций.
