Featured image of post Почему компьютеры не могут точно сложить 0.1 + 0.2: Наука о числах с плавающей запятой

Почему компьютеры не могут точно сложить 0.1 + 0.2: Наука о числах с плавающей запятой

Введение: Загадка «0.1 + 0.2»

Каждый программист рано или поздно сталкивается с этим парадоксом:

1
2
3
4
console.log(0.1 + 0.2);
// Результат: 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);
// Результат: false

Как современные суперкомпьютеры, моделирующие квантовые системы, могут ошибаться в сложении чисел уровня начальной школы? Это не ошибка в языке программирования. Любой современный процессор на Земле выдает строго одинаковый результат: 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$.
$$(0.1)_{10} = (0.0001100110011\dots)_2$$

Поскольку разрядность регистров конечна, неизбежно происходит усечение числа.


2. Стандарт IEEE 754 и сложение в процессоре

По стандарту IEEE 754 (binary64) число кодируется в 64 битах:

  • 1 бит знака
  • 11 бит порядка (со смещением 1023)
  • 52 бита мантиссы (53 бита эффективной точности благодаря неявному первому биту).
$$\mathbf{0.300000000000000044408920985...}$$

Поэтому проверка 0.1 + 0.2 === 0.3 возвращает false.


3. Исторические катастрофы и практические правила

  • Трагедия с ракетой Patriot (1991): Накопление погрешности таймера на 0.1 с за 100 часов непрерывной работы привело к временному сдвигу на 0.34 с. Радар потерял ракету Scud на дистанции 687 метров, в результате чего погибло 28 солдат.
  • Взрыв ракеты Ариан-5 (1996): Переполнение при конвертации 64-битного числа с плавающей запятой в 16-битное целое число привело к самоуничтожению ракеты стоимостью 500 миллионов долларов.

Правила для разработчиков:

  1. Никогда не сравнивайте числа с плавающей запятой через ==. Используйте машинный эпсилон $\epsilon$ (math.isclose).
  2. В финансовых системах используйте целые числа (копейки/центы) или специализированные десятичные типы (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. Алгоритм компенсационного суммирования Кэхэна

Для устранения эффекта поглощения при суммировании миллионов чисел лауреат премии Тьюринга Уильям Кэхэн создал элегантный алгоритм:

1
2
3
4
5
6
7
8
9
def kahan_sum(numbers):
    total = 0.0
    c = 0.0  # Переменная для компенсации погрешности
    for x in numbers:
        y = x - c
        t = total + y
        c = (t - total) - y  # Извлечение отброшенных младших битов
        total = t
    return total

Этот алгоритм сводит накапливаемую погрешность практически к нулю даже при миллиардах итераций.

comments powered by Disqus