Ты бы сильно удивился, если бы открыл калькулятор на компьютере, набрал 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) (пять умножить на десять в минус двадцать первой степени)

Теперь умножаем по обычным математическим правилам:

  1. Умножаем мантиссы: 3 × 5 = 15
  2. Складываем степени десяти: 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, выполняй цепочку вычислений целиком, а потом округляй до нужного количества знаков. Это сводит ошибку к минимуму.

Куда подойдут числа с плавающей запятой

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

Где нужны точные дроби

Финансы. Любой лишний цент может стоить миллионы, если операция повторяется часто. Координаты космических аппаратов. Миллиметры на Земле — километры в космосе. Математическое моделирование, где сравнивают очень близкие значения.