Проблема плавающей запятой
Ты бы сильно удивился, если бы открыл калькулятор на компьютере, набрал 0.1 + 0.2 и увидел 0.30000000000000004? Может показаться, что программа сломалась, но на самом деле перед тобой нормальная работа того, что называется «плавающей запятой» (по-английски floating point).
Почему в памяти не получается ровная десятая
Когда ты говоришь компьютеру 0.1, он переводит это в свою «речь» — длинную цепочку нулей и единиц. В двоичной системе разрешены только деления на 2: 1/2, 1/4, 1/8, 1/16 и так далее. Поделить «пирог» на десять равных частей, используя только эти «половинки», невозможно — придётся всё время брать чуть больше или чуть меньше. Так никогда и не найдя идеальную десятую. В результате в памяти компьютера хранится не точная «0,1», а приближённая версия:
0.0001100110011...
Здесь группа «0011» будет повторяться бесконечно. Но компьютер не умеет хранить бесконечность: у него есть жёстко отведённое место — 52 «ячейки» для дробной части (мантиссы). Он просто берёт первые 52 бита этой бесконечной строки и обрезает остальное. Получается чуть-чуть неверное число, внутри оно примерно равно:
0.10000000000000000555...
То есть вместо идеальной 0.1 в памяти лежит 0.10000000000000000555..., а мы видим лишь начало этой длинной строки.
Давай вспомним дробь 1/3. В школьном калькуляторе она превращается в 0.333333… — тройка повторяется бесконечно. Мы знаем, что конца этого числа не будет, потому что в десятичном счислении единицу нельзя ровно поделить на три.
С 0.1 происходит то же самое, только: в десятичной системе она представлена идеально (одна цифра после запятой — и всё), а вот в двоичной делится бесконечно — приходится каждый раз брать то чуть больше, то чуть меньше, и в итоге выходит бесконечная цепочка ...0011 0011 0011
Здесь важен сам принцип: если знаменатель дроби содержит простые множители, которых нет в базе системы счисления (у десятичной это 2 и 5, у двоичной — только 2), то дробь растягивается в бесконечную ленту повторяющихся цифр. Поэтому 0.5 (1/2) представляется точно и в десятичной, и в двоичной. А вот 0.1 (1/10) «рвётся» в двоичной, потому что деление на 5 — чужая операция для мира нулей и единиц.
Так что, когда мы говорим «дробь бесконечна», это совсем не экзотика: такая же ситуация происходит каждый раз, когда пишешь 0.3333… на бумажке. Просто в компьютере роль бесконечной тройки выполняет бесконечная последовательность единиц и нулей, которую приходится обрезать, чтобы число поместилось в выделенные 52 бита.
Откуда вообще взялась «плавающая точка»
Компьютерам нужно уметь работать с очень большими числами (например, расстояние до звёзд) и с очень маленькими (доли секунды в анимации). Хранить каждое число обычной длинной десятичной записью, например огромное 300000000000000000000000 или очень микроскопическое 0.000000000000000000005 было бы неудобно и медленно. Поэтому придумали систему с плавающей точкой — то есть научную нотацию.
Чтобы понять удобство, давай попробуем умножить два числа друг с другом:
300000000000000000000000 × 0.000000000000000000005
Можно попытаться… но давай подойдём с другой стороны. Мы можем представить эти два числа в виде научной нотации и совершить операцию станет гораздо проще. Вот как это выглядит:
300 000 000 000 000 000 000 000 = 3 × 10^23 (три умножить на десять в двадцать третьей степени)
0.000 000 000 000 000 000 005 = 5 × 10^(-21) (пять умножить на десять в минус двадцать первой степени)
Теперь умножаем по обычным математическим правилам:
- Умножаем мантиссы:
3 × 5 = 15 - Складываем степени десяти:
23 + (–21) = 2
Получаем:
15 × 10^2 = 1500
Видишь? Вместо того чтобы писать в столбик сотни нулей, достаточно двух-трёх чисел и простой арифметики с показателями степени. Именно это и даёт формат с плавающей запятой: ты оперируешь компактными мантиссами и экспонентами, а не длинными «несуразными» записями с десятками нулей.
Теперь, что касается компьютеров и их двоичной системы. Тут числа представляются точно так же, только основанием счисления будет не 10, а 2, т.к. система двоичная. Простыми словами, умножать мантиссу мы будет на 2 в степени, а не на 10.
Возьмём значимую часть: 1.0010011. Допустим, к ней приписана степень 2^(-3). Это значит:
1.0010011 × 2^-3 = 0.0010010011
Если степень поменять на +4, запятая «уплывёт» вправо:
1.0010011 × 2^4 = 10010.011₂
Запятая не хранится где-то внутри числа — она мысленно «прыгает» благодаря тому, что мы умножаем или делим на 2 в нужной степени.
В формате double (64 бита) всё распределено так:
- 1 бит — знак (плюс или минус),
- 11 бит — степень двойки (от -1022 до +1023),
- 52 бита — значащая часть.
Чем длиннее мантисса, тем больше точных цифр можно уместить; чем шире поле степени, тем больший диапазон чисел доступен.
Откуда берётся лишняя «четвёрка»
Мы уже знаем, что 0.1 хранится приближённо. Если перевести сохранённую в памяти двоичную мантиссу обратно в десятичные цифры, получаем:
0.1 (в памяти) ≈ 0.10000000000000000555…
0.2 (в памяти) ≈ 0.20000000000000001110…
Эти длинные десятичные хвосты — простой результат пересчёта того же «0011 0011…» из двоичной системы обратно в десятичную: точного совпадения нет, поэтому проявляются дополнительные пятёрки и единицы после многих знаков.
Складываем два приближения бинарно и получаем новое двоичное число, которое опять переводим в десятичную форму:
0.30000000000000004441…
Калькулятор показывает только 17 значащих цифр, поэтому видим укороченный вариант 0.30000000000000004. «Четвёрка» — это не прихоть программы, а видимая вершина двух накопленных крошечных ошибок округления: одна пришла из «десятой», другая — из «двадцатой».
Таким образом, запись 0.0001100110011... в компьютере превращается в 0.10000000000000000555... просто потому, что перевод назад в десятичную систему выводит на экран все спрятанные неточности, накопленные после неизбежного обрезания бесконечной двоичной дроби.
Накопление погрешностей в реальных задачах
Погрешность мала сама по себе, но может расти. В 1991 году зенитная система Patriot считала время шагами по 0.1 секунды. После 100 часов работы накопилось около трети секунды неточности. Этого хватило, чтобы радар промахнулся по ракете-цели. Случай трагичный, но хорошо показывает, что «хвостики» нельзя игнорировать, если цена ошибки велика.
Способы обойти проблему
-
Хранить копейки целым числом. Если тебе важны деньги, не используй 12.34 как float. Сохрани 1234 копейки в переменной-целом. Так не появятся неожиданные хвосты. Это называется «фиксированная точка» (fixed-point).
-
Десятичный тип. В некоторых языках есть специальный тип
decimal, который использует систему счисления по основанию 10, а не 2. Дробь 0.1 представляется точно. Только вот всё этоработает медленнее. -
Округлять в конце расчётов. Если всё-таки работаешь с float, выполняй цепочку вычислений целиком, а потом округляй до нужного количества знаков. Это сводит ошибку к минимуму.
Куда подойдут числа с плавающей запятой
Графика и звук. Там важно быстро считать, а небольшая погрешность не видна глазу и уху. Физика игр. Отклонение в миллионную долю метра не влияет на поведение персонажей. Статистика больших массивов. Погрешности растворяются в среднем значении.
Где нужны точные дроби
Финансы. Любой лишний цент может стоить миллионы, если операция повторяется часто. Координаты космических аппаратов. Миллиметры на Земле — километры в космосе. Математическое моделирование, где сравнивают очень близкие значения.