Числа с плавающей точкой

Предпосылки: Двоичная система и байты, Целые числа.

Побитовые операции | Кодирование текста

36.6 градусов температуры, 3.14 для площади круга, 0.001 секунды задержки — целых чисел для этого недостаточно. Нужен способ записать дробную часть теми же битами, из которых состоят целые числа.

Двоичные дроби

Позиционная запись работает и после точки. В десятичной системе разряды после точки — это 10^(-1) = 0.1, 10^(-2) = 0.01, 10^(-3) = 0.001. В двоичной — то же самое, но степени двойки:

ПозицияВесДесятичное значение
-12^(-1)0.5
-22^(-2)0.25
-32^(-3)0.125
-42^(-4)0.0625

Как перевести число с дробной частью — например, 3.625 — в двоичную? Целую и дробную часть переводим отдельно.

Целая часть переводится так же, как в двоичной системе: 3 = 2 + 1 = 11₂.

Дробную часть переводим последовательным умножением на 2:

0.625 × 2 = 1.25  --> 1   (записываем 1, продолжаем с 0.25)
0.25  × 2 = 0.5   --> 0
0.5   × 2 = 1.0   --> 1   (дробная часть = 0, готово)

Дробная часть: 0.625₁₀ = 0.101₂. Склеиваем целую и дробную: 3.625₁₀ = 11.101₂.

Проверка — складываем веса позиций, где стоит единица:

11.101₂ = 1×2 + 1×1 + 1×0.5 + 0×0.25 + 1×0.125 = 2 + 1 + 0.5 + 0.125 = 3.625₁₀ ✓

Не все десятичные дроби переводятся ровно. Число 0.1₁₀ в двоичной даёт бесконечную периодическую дробь — к этому мы вернёмся позже, и это окажется главным источником неприятностей.

На бумаге можно написать 11.101₂ — три бита целой части, точка, три бита дробной. Но в памяти компьютера нет символа «точка». Есть только биты — фиксированное количество нулей и единиц. Если записать 11101 подряд, процессор не знает, где заканчивается целая часть и начинается дробная. Нужно соглашение: какие биты отвечают за целую часть, а какие — за дробную.

Фиксированная точка: простое решение и его границы

Первая идея: разделить биты пополам. Старшие биты — целая часть, младшие — дробная. Например, в 16-битном числе 8 бит хранят целую часть, 8 бит — дробную. Такой формат называется фиксированная точка (fixed point), потому что граница между целой и дробной частью всегда на одном месте.

  Формат 8.8 (8 [бит](binary-and-bytes.md) целой части, 8 [бит](binary-and-bytes.md) дробной):
 
  [IIIIIIII.FFFFFFFF]
   целая     дробная
   часть     часть
 
  Пример: 00000011.10000000 = 3 + 0.5 = 3.5

Дробная часть из 8 бит даёт точность до 1/256 = 0.00390625 — минимальный шаг между соседними значениями. Для денежных расчётов, где нужны доллары и центы, такой точности хватает.

Арифметика с фиксированной точкой простая: сложение и вычитание — как у целых чисел (точка на одном месте, складываем бит за битом). Умножение чуть сложнее, но предсказуемо. Нет неожиданных ошибок округления — каждое значение представлено точно в пределах разрешения формата. Поэтому фиксированная точка хорошо подходит для денежных расчётов: 100 центов = 1 доллар, шаг 0.01, все операции точные.

Но диапазон тесный. Формат 8.8 — беззнаковое целое от 0 до 255, дробная часть от 0 до 255/256 ≈ 0.996. Максимальное число — 255.996. Если взять 16.16 (32 бита), диапазон вырастет до 0-65535.9999… Для повседневной арифметики достаточно, но мир полон чисел на разных концах шкалы: масса электрона — 9.109 * 10^(-31) кг, число Авогадро — 6.022 * 10^23. Ни один формат с фиксированной точкой не вместит оба значения: для огромных чисел нужно много бит целой части, для крошечных — много бит дробной. Каждый раз, когда мы добавляем точность дробной части, мы отбираем диапазон у целой, и наоборот.

Нужен формат, в котором одни и те же 32 бита могут описать и 10^23, и 10^(-31). Фиксированная точка этого не позволяет — точка стоит на месте, и биты жёстко распределены между целой и дробной частями.

Научная запись: точка, которая плавает

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

6 022 000 000 000 000 000 000 00  -->  6.022 * 10^23
0.000 000 000 000 000 000 000 000 000 000 9109  -->  9.109 * 10^(-31)

Формат один и тот же: знак, значащие цифры и показатель степени, который указывает, куда сдвинуть точку. Точка «плавает» — перемещается в зависимости от показателя. Значащих цифр мало (4 в примерах выше), но показатель может быть и +23, и -31.

В двоичной системе работает та же идея: любое ненулевое число можно представить в виде +-1.дробная_часть * 2^показатель. Например:

  0.15625 в десятичной
= 0.00101 в двоичной
= 1.01 * 2^(-3)   (сдвинули точку на 3 позиции вправо)

Вместо фиксированного разделения «столько-то бит на целую часть, столько-то на дробную» мы выделяем биты на показатель степени. Показатель подвигает точку туда, где она нужна. Это и есть числа с плавающей точкой (floating point) — точка не зафиксирована, а плавает благодаря показателю.

Компромисс: мы жертвуем точностью (количеством значащих цифр), но взамен получаем огромный диапазон. Четыре значащие цифры покрывают и 10^23, и 10^(-31). Фиксированная точка — одинаковый шаг между соседними числами по всей шкале. Плавающая точка — шаг зависит от масштаба: возле нуля числа расположены плотно, далеко от нуля — разреженно. Конкретный пример: в float32 между 1.0 и 2.0 лежит 2^23 = 8 388 608 представимых чисел — шаг между соседними составляет примерно 0.00000012. Между 1 000 000 и 2 000 000 представимых чисел столько же (8 388 608), но шаг уже примерно 0.12. Между 10^30 и 2 * 10^30 — тоже 8 388 608, но шаг ≈ 10^23. Точность всегда одинакова в относительных терминах (~7 значащих цифр), но абсолютная погрешность растёт вместе с масштабом числа.

IEEE 754: стандартный формат

Идея ясна: знак, значащие цифры, показатель. Но как именно упаковать их в 32 или 64 бита? Сколько бит выделить на показатель, а сколько на значащие цифры? Как кодировать отрицательный показатель? Что делать, если результат бесконечен или не определён?

К 1980-м годам разные производители процессоров отвечали на эти вопросы по-своему: одна и та же программа давала разные результаты на разных машинах. В 1985 году IEEE (Institute of Electrical and Electronics Engineers — Институт инженеров по электротехнике и электронике) принял стандарт IEEE 754, который зафиксировал единый формат. Сегодня его поддерживают практически все процессоры.

Число состоит из трёх полей: знак (sign), показатель степени (exponent, экспонента) и дробная часть значащего числа (mantissa, мантисса):

float32 (32 [бита](binary-and-bytes.md), одинарная точность):
 
  [S|EEEEEEEE|MMMMMMMMMMMMMMMMMMMMMMM]
   1    8                23              = 32 [бита](binary-and-bytes.md)
 
float64 (64 [бита](binary-and-bytes.md), двойная точность):
 
  [S|EEEEEEEEEEE|MMMM...........MMMM]
   1      11              52             = 64 [бита](binary-and-bytes.md)
  • S (1 бит) — знак: 0 = положительное, 1 = отрицательное.
  • E (8 или 11 бит) — показатель степени (экспонента). Определяет масштаб числа.
  • M (23 или 52 бита) — мантисса. Определяет значащие цифры.

Почему именно такое распределение бит? Больше бит экспоненте — шире диапазон, но меньше точность. Больше бит мантиссе — выше точность, но уже диапазон. 8 бит экспоненты в float32 покрывают порядки от 10^(-38) до 10^38 — достаточно для большинства физических величин. 23 бита мантиссы дают около 7 десятичных цифр точности. Для float64 баланс смещён: 11 бит экспоненты (до 10^308) и 52 бита мантиссы (~15 десятичных цифр).

Смещение экспоненты

Три поля определены, но остаётся вопрос: показатель степени может быть отрицательным. Число 0.15625 = 1.01 × 2⁻³, здесь экспонента = −3. Как хранить отрицательную экспоненту в поле из 8 бит?

Первая идея — дополнительный код, как для целых чисел. Но посмотрим, что получится. Допустим, у нас два числа:

Число A: экспонента +5  → в доп. коде: 00000101
Число B: экспонента -3  → в доп. коде: 11111101

Число A больше числа B (2⁵ > 2⁻³). Но если сравнить хранимые экспоненты как обычные беззнаковые числа — 11111101 (253) больше 00000101 (5). Получается, меньшее число «выглядит» большим. Чтобы сравнивать правильно, процессору пришлось бы сначала распознавать знак экспоненты — лишняя работа.

Решение проще: смещение (bias). К настоящей экспоненте прибавляется фиксированная величина, чтобы хранимое значение всегда было неотрицательным.

Для float32 смещение = 127. Хранимая экспонента = настоящая + 127:

  • настоящая экспонента 0 хранится как 127 (01111111)
  • настоящая экспонента +10 хранится как 137 (10001001)
  • настоящая экспонента -3 хранится как 124 (01111100)

Для float64 смещение = 1023, по той же логике: 2^(11-1) - 1 = 1023.

Теперь большая хранимая экспонента всегда означает большее число — побитовое сравнение работает правильно для чисел одного знака. Это не случайное удобство. Конструкторы IEEE 754 специально расположили поля в порядке «знак, экспонента, мантисса», чтобы для положительных чисел побитовое сравнение как беззнаковых целых давало правильный порядок. Одна схема сравнения обслуживает и целые, и дробные числа — это упрощает аппаратуру.

Скрытый бит: бесплатная точность

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

В двоичной научной записи любое ненулевое число имеет вид 1.что-то * 2^N. Цифра перед точкой всегда 1 — других ненулевых цифр в двоичной системе нет. Раз эта единица всегда одинаковая, зачем тратить на неё бит?

IEEE 754 не хранит эту единицу. Мантисса записывает только цифры после точки. При чтении числа процессор автоматически подставляет 1 перед точкой. Это называется скрытый бит (hidden bit, implicit bit).

23 бита мантиссы в float32 хранят 23 цифры после точки, но с учётом скрытой единицы значащая часть состоит из 24 двоичных цифр. Один бит точности — бесплатно. По аналогии: если все номера домов на улице начинаются с «1», печатать эту единицу на каждом конверте расточительно — достаточно запомнить правило.

Для float64 скрытый бит превращает 52 хранимых бит в 53 значащих.

Пошаговый пример: кодируем 0.15625

Переведём число 0.15625 в формат float32.

Шаг 1. Перевод в двоичную систему. Последовательно умножаем дробную часть на 2:

0.15625 * 2 = 0.3125  --> 0
0.3125  * 2 = 0.625   --> 0
0.625   * 2 = 1.25    --> 1   (отбрасываем целую часть)
0.25    * 2 = 0.5     --> 0
0.5     * 2 = 1.0     --> 1   (отбрасываем целую часть)

Результат: 0.15625 = 0.00101 в двоичной.

Шаг 2. Нормализация. Сдвигаем точку так, чтобы перед ней стояла единица:

0.00101 = 1.01 * 2^(-3)

Шаг 3. Заполняем поля:

  • Знак = 0 (число положительное)
  • Настоящая экспонента = -3, хранимая = -3 + 127 = 124 = 01111100 в двоичной
  • Мантисса = 01 (цифры после точки в 1.01; скрытая единица не хранится), дополненная нулями до 23 бит: 01000000000000000000000

Результат:

0  01111100  01000000000000000000000
S  экспон.   мантисса

Проверка: знак 0 (плюс), экспонента 124 - 127 = -3, мантисса 1.01 (скрытый бит + 01). Число = +1.01 * 2^(-3) = +0.00101 = 0.15625. Верно.

Попробуем ещё одно число — отрицательное: -6.5.

6.5 = 110.1 в [двоичной](binary-and-bytes.md) = 1.101 * 2^2
 
Знак       = 1 (отрицательное)
Экспонента = 2 + 127 = 129 = 10000001
Мантисса   = 101 (после точки в 1.101), дополняем нулями до 23 [бит](binary-and-bytes.md):
             10100000000000000000000
 
Результат: 1  10000001  10100000000000000000000

Обратная операция — декодирование — работает в обратном порядке: извлекаем поля, вычитаем смещение из экспоненты, подставляем скрытый бит, восстанавливаем число.

Округление

Когда результат вычисления не укладывается точно в мантиссу, его нужно округлить. IEEE 754 определяет несколько режимов, но по умолчанию используется round to nearest, ties to even (округление к ближайшему, при равном расстоянии — к чётному). Если результат ровно посередине между двумя представимыми числами, выбирается то, у которого младший бит мантиссы = 0.

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

Помимо выбора нейтрального режима, стандарт фиксирует и саму точность. IEEE 754 гарантирует: результат любой базовой операции (сложение, вычитание, умножение, деление, квадратный корень) — это точный математический результат, округлённый по выбранному режиму. То есть операция выполняется так, как если бы у процессора была бесконечная точность, и только потом результат округляется до мантиссы. Ошибка каждой отдельной операции — не больше половины шага (0.5 ulp, unit in the last place — единица в последнем разряде мантиссы). Это сильная гарантия: результат настолько точен, насколько позволяет формат.

Специальные значения

Округление решает проблему конечной точности для «обычных» чисел. Но что происходит, когда результат выходит за пределы диапазона — например, деление на ноль? Или когда операция математически не определена — корень из отрицательного числа? Для таких случаев стандарт резервирует специальные комбинации бит.

Два значения экспоненты зарезервированы: все нули (00000000) и все единицы (11111111 для float32). Они кодируют случаи, которые не вписываются в обычную схему.

Ноль. Экспонента = 0, мантисса = 0. Формально 1.0 * 2^(-127) не равно нулю, поэтому ноль закодирован как исключение. Бит знака при этом работает: существуют +0 (все биты нули) и -0 (знаковый бит = 1, остальное нули). По стандарту +0 = -0 при сравнении, но знак сохраняется — он может иметь значение при делении: 1.0 / (+0) = +бесконечность, 1.0 / (-0) = -бесконечность.

Бесконечность (infinity). Экспонента = все единицы, мантисса = 0. Результат операций, выходящих за диапазон: 1.0 / 0.0 = +бесконечность, -1.0 / 0.0 = -бесконечность. Бесконечность участвует в дальнейших вычислениях по понятным правилам: бесконечность + 1 = бесконечность, бесконечность * (-1) = -бесконечность.

NaN (Not a Number — не число). Экспонента = все единицы, мантисса отлична от нуля. Результат неопределённых операций: 0.0 / 0.0, квадратный корень из отрицательного числа, бесконечность минус бесконечность. Зачем отдельное значение, а не ошибка? Чтобы длинная цепочка вычислений не прерывалась на середине. NaN «пропитывает» результат: любая операция с NaN даёт NaN. Программа доходит до конца, и программист видит NaN в итоге — сигнал, что где-то раньше произошла неопределённая операция.

NaN обладает уникальным свойством: NaN не равен ничему, включая самого себя. Выражение NaN == NaN возвращает ложь. Это единственное значение, для которого x != x — истина, и этим свойством пользуются для проверки: если x != x, значит x — это NaN. Почему такой странный выбор? Потому что NaN — это «ответа нет». Если спросить «равен ли ответ-которого-нет самому себе», единственный непротиворечивый ответ — нет.

Специальные значения float32:
 
Экспонента   Мантисса    Значение
00000000     = 0         +-0 (знак из [бита](binary-and-bytes.md) S)
00000000     != 0        денормализованные числа (см. ниже)
11111111     = 0         +-бесконечность
11111111     != 0        NaN
01-11111110  любая       обычные (нормализованные) числа

Денормализованные числа: плавный переход к нулю

Наименьшее нормализованное число в float32 — 1.0 * 2^(-126) ≈ 1.18 * 10^(-38). Следующее за ним в сторону нуля — это ноль? Тогда между нулём и этим числом была бы пустота: числа вроде 0.5 * 10^(-38) невозможно представить. Хуже того, разность двух близких маленьких чисел может дать ноль, хотя числа не равны — вычитание теряет информацию.

Денормализованные числа (denormalized, subnormals) заполняют этот промежуток. У них экспонента = 0 и мантисса отлична от нуля. Отличие от обычных чисел: скрытый бит не подставляется. Вместо 1.мантисса число интерпретируется как 0.мантисса * 2^(-126).

Нормализованные:     1.мантисса * 2^(экспонента - 127)
                     скрытый [бит](binary-and-bytes.md) = 1
 
Денормализованные:   0.мантисса * 2^(-126)
                     скрытый [бит](binary-and-bytes.md) = 0, экспонента фиксирована

Наименьшее денормализованное float32: 0.00000000000000000000001 * 2^(-126) = 2^(-149) ≈ 1.4 * 10^(-45). Числа плавно уменьшаются от наименьшего нормализованного к нулю, теряя по одному биту точности на каждом шаге. Это называется постепенное исчезновение (gradual underflow): вместо резкого прыжка «число → ноль» точность деградирует плавно.

Цена: денормализованные числа менее точны (меньше значащих бит) и на многих процессорах вычисляются в десятки раз медленнее, чем нормализованные — аппаратура оптимизирована для нормализованных чисел, а денормализованные обрабатываются отдельным, медленным путём (microcode, микрокод — встроенные подпрограммы процессора). Некоторые процессоры позволяют включить режим «flush to zero» (сброс в ноль): любой денормализованный результат заменяется нулём. Это быстрее, но ломает свойство «если a != b, то a - b != 0», которое денормализованные числа гарантируют.

Без денормализованных чисел шкала выглядит так:

... smallest_normal ... 0    <-- пустота между 0 и smallest_normal

С денормализованными:

... smallest_normal ... denorm ... denorm ... denorm ... 0
                        менее точные, но заполняют пробел

Почему 0.1 + 0.2 не равно 0.3

Это, пожалуй, самый известный эффект чисел с плавающей точкой. Разберём его по шагам.

Проблема: бесконечные двоичные дроби. Число 1/3 в десятичной системе — бесконечная дробь: 0.3333… Её невозможно записать точно конечным количеством десятичных цифр. Аналогичная ситуация возникает в двоичной системе, но с другими числами.

Переведём 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.4 * 2 = 0.8  --> 0
...

0.1 в двоичной = 0.0001100110011001100110011… — бесконечная периодическая дробь с периодом 0011. В float64 мантисса — 52 бита. Бесконечная дробь обрезается, и сохранённое значение чуть больше, чем 0.1:

сохранённое 0.1 = 0.1000000000000000055511151231257827021181583404541015625

То же самое с 0.2 — тоже бесконечная двоичная дробь, тоже обрезается с небольшим избытком.

Сложение. Процессор складывает два приближённых значения. Результат: 0.30000000000000004 (в float64). Это не 0.3.

А что со значением 0.3? Число 0.3 — тоже бесконечная двоичная дробь, и оно тоже округляется при сохранении. Но округляется иначе, чем сумма 0.1 + 0.2. Поэтому stored(0.1) + stored(0.2) != stored(0.3), хотя математически 0.1 + 0.2 = 0.3.

Это не ошибка реализации — это фундаментальное следствие того, что конечное количество бит не может точно представить все десятичные дроби. В десятичной системе та же проблема с 1/3: 0.333 + 0.333 + 0.333 = 0.999, а не 1.000.

Какие десятичные дроби представляются в двоичной точно? Только те, чей знаменатель — степень двойки: 0.5 (= 1/2), 0.25 (= 1/4), 0.125 (= 1/8), 0.375 (= 3/8). Числа вроде 0.1 (= 1/10), 0.2 (= 1/5), 0.3 (= 3/10) — нет, потому что 10 = 2 * 5, а множитель 5 не раскладывается на двойки. Любая дробь со знаменателем, содержащим множитель 5, даёт бесконечную периодическую двоичную дробь.

Практические следствия

Сравнение: не используйте точное равенство. Выражение a == b для чисел с плавающей точкой почти всегда ненадёжно. Вместо этого проверяют, что разность достаточно мала:

epsilon = 0.000001
если |a - b| < epsilon, то a и b "равны"

Величина epsilon зависит от задачи и от масштаба чисел. Для координат на экране (пиксели) достаточно 0.001. Для физических симуляций может потребоваться 10^(-12). Универсального значения нет — абсолютный epsilon, подходящий для чисел порядка 1.0, будет бессмысленно грубым для чисел порядка 10^(-10) и бессмысленно строгим для чисел порядка 10^10. Более надёжный подход — относительное сравнение: |a - b| < epsilon * max(|a|, |b|).

Деньги: не храните как float. Сумма 0.10 доллара + 0.20 доллара должна быть ровно 0.30 доллара, без отклонений. Числа с плавающей точкой этого не гарантируют. В бухгалтерии ошибка на один цент при миллионах транзакций превращается в тысячи потерянных (или лишних) долларов. Решения: хранить суммы в центах как целые числа (1099 вместо 10.99), использовать специализированные десятичные типы (decimal/numeric) в базах данных — они хранят число в десятичном виде и не вносят двоичных ошибок округления — или формат с фиксированной точкой.

Точность float32 и float64. Мантисса float32 — 23 бита + скрытый бит = 24 двоичные цифры. 2^24 = 16 777 216 — это порядка 7 десятичных цифр. Начиная с 8-й десятичной цифры float32 теряет точность. Для float64: 52 + 1 = 53 двоичные цифры, 2^53 ≈ 9.0 * 10^15 — около 15-16 десятичных цифр.

ФорматМантисса (бит)Десятичных цифрМакс. экспонентаДиапазон
float3223 + 1~7+127≈ +-3.4 * 10^38
float6452 + 1~15-16+1023≈ +-1.8 * 10^308

Потеря значимости при вычитании. Когда два близких числа вычитаются, старшие значащие цифры сокращаются, и в результате остаются только младшие — те, что наименее точны.

Пример в десятичной системе с 7 значащими цифрами:

a = 1.000001(2...)   (цифры после 7-й неизвестны)
b = 1.000000(8...)
 
a - b = 0.000000(4...)

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

Это называется catastrophic cancellation (катастрофическая потеря значимости). Числовые алгоритмы специально перестраивают формулы, чтобы избежать вычитания близких чисел. Классический пример: формула корней квадратного уравнения x = (-b + sqrt(b^2 - 4ac)) / 2a теряет точность, когда b^2 значительно больше 4ac — sqrt(b^2 - 4ac) оказывается близок к b, и вычитание уничтожает значащие цифры.

Порядок операций имеет значение. В математике a + b + c = c + b + a. Для чисел с плавающей точкой результат может зависеть от порядка сложения. Если a = 10^20, b = -10^20, c = 1.0, то (a + b) + c = 0 + 1 = 1.0, но a + (c + b) может дать не 1.0 — при сложении a + c число 1.0 теряется на фоне 10^20 (оно меньше одного шага мантиссы при таком масштабе). Ассоциативность сложения не выполняется для float.

Накопление ошибок. Нарушение ассоциативности — проявление одиночной ошибки. Но ошибки могут и суммироваться. При каждой операции с плавающей точкой результат округляется. Одно округление — ничтожная погрешность. Но в длинных вычислениях (тысячи итераций симуляции, суммирование большого массива) ошибки могут накапливаться. Если наивно суммировать миллион чисел порядка 1.0, итоговая ошибка может достигать нескольких единиц. Алгоритм Kahan summation (суммирование Кэхана) — один из способов борьбы: он хранит отдельную переменную для накопившейся ошибки округления и компенсирует её на каждом шаге, уменьшая ошибку с O(N) до O(1) независимо от длины суммы.

Целые числа в float64. Мантисса float64 — 53 бита. Целые числа от 0 до 2^53 (9 007 199 254 740 992) представляются точно: каждому такому целому соответствует ровно одно значение float64 без округления. После 2^53 шаг между соседними представимыми числами становится больше 1, и не все целые числа можно представить: 2^53 + 1 округляется обратно до 2^53. Поэтому, например, идентификаторы в JSON (который по спецификации использует float64 для всех чисел) безопасны только до 2^53 — для больших ID нужна строковая передача.


Целые числа, дроби, плавающая точка — всё это про хранение величин. Но на экране отображаются буквы. Байт 01000001 может оказаться числом 65, а может — буквой A. Где записано, какой это символ, и как вообще биты становятся текстом?

Sources


Побитовые операции | Кодирование текста