Плавающая точка IEEE 754: как компьютер хранит дроби
Урок объясняет внутреннее устройство типа float: формат IEEE 754, почему дробей «бесконечно много», а битов конечно, и что из этого следует.
IEEE 754 double — стандарт, по которому вещественное число хранится в 64 битах как знак (1 бит), порядок (11 бит) и мантисса (52 бита): $x = \pm 1.\text{mantissa} \times 2^{\text{порядок}}$.
Зачем вообще «плавающая» точка
Компьютеру нужно одним типом покрыть и заряд электрона ($1.6 \times 10^{-19}$), и массу Солнца ($2 \times 10^{30}$). Числа с фиксированной точкой (как у целых, но с запятой в фиксированном месте) для этого не годятся: либо не хватит дробной части для крошечных величин, либо целой — для гигантских. Решение — плавающая точка: храним значащие цифры (мантиссу) отдельно от масштаба (порядка), как в научной записи $1.6 \times 10^{-19}$, только в двоичной системе. «Точка плавает» — её положение задаётся порядком.
Тип float в Python — это IEEE 754 double, 64 бита. Из них: 1 бит на знак, 11 на порядок (диапазон примерно от $10^{-308}$ до $10^{308}$), 52 на мантиссу (это и есть точность — около 15–16 значащих десятичных цифр).
import math, sys
print("макс. конечное число:", sys.float_info.max)
print("мин. нормальное: ", sys.float_info.min)
print("значащих десятичных цифр:", sys.float_info.dig)
# Точность одинакова не везде: между большими числами «дырки» больше
for x in [1.0, 1e6, 1e15, 1e16]:
след = math.nextafter(x, math.inf) # ближайшее большее представимое
print(f"около {x:>7g}: расстояние до соседа = {след - x:.3e}")
Вывод:
макс. конечное число: 1.7976931348623157e+308 мин. нормальное: 2.2250738585072014e-308 значащих десятичных цифр: 15 около 1: расстояние до соседа = 2.220e-16 около 1e+06: расстояние до соседа = 1.164e-10 около 1e+15: расстояние до соседа = 1.250e-01 около 1e+16: расстояние до соседа = 2.000e+00
Ключевой вывод: представимые числа расположены неравномерно. Около единицы соседние float-ы отстоят на $2.2 \times 10^{-16}$, а около $10^{16}$ — уже на 2. То есть при больших значениях целые числа перестают быть представимыми точно! $10^{16} + 1$ может равняться $10^{16}$. Точность — относительная, а не абсолютная.
Двоичные дроби: почему 0.1 «некруглое»
В десятичной системе $1/3 = 0.333\ldots$ — бесконечная дробь, её не записать точно конечным числом цифр. В двоичной системе ровно та же беда постигает $0.1$: число $1/10$ в двоичной записи периодично — $0.0001100110011\ldots$ — и в 52 бита мантиссы укладывается лишь приближённо. Компьютер хранит не $0.1$, а ближайшее представимое число, чуть-чуть отличающееся.
# Покажем «настоящее» значение 0.1, спрятанное за красивым выводом
from decimal import Decimal
print("то, что мы написали: 0.1")
print("то, что реально хранится:")
print(Decimal(0.1))
Вывод:
то, что мы написали: 0.1 то, что реально хранится: 0.1000000000000000055511151231257827021181583404541015625
Вот оно — «0.1» на самом деле чуть больше. Обычный print(0.1) показывает 0.1, потому что Python печатает кратчайшую десятичную строку, которая округляется обратно в то же самое представимое число. Но внутри сидит этот длинный хвост, и именно из-за него начинаются сюрпризы.
Как работает под капотом
Каждое арифметическое действие в IEEE 754 выполняется так: вычисляется точный результат, затем он округляется к ближайшему представимому числу. Относительная ошибка одного округления не превышает половины «машинного эпсилона» (про него — следующий урок). Для одной операции это $10^{-16}$ — пренебрежимо. Проблемы начинаются, когда таких операций миллионы и ошибки накапливаются, или когда формула устроена так, что крошечная ошибка раздувается (вычитание близких чисел).
| Что | Значение для double |
| Битов всего | 64 (1 знак + 11 порядок + 52 мантисса) |
| Значащих десятичных цифр | $\approx$ 15–16 |
| Диапазон по модулю | примерно 2.2e-308 … 1.8e+308 |
| Особые значения | inf, -inf, nan, -0.0 |
Частые ошибки
- Сравнивать float через
==. Из-за округления $0.1 + 0.2 = 0.3$ ложно. Сравнивайте через допуск: $|a - b| \lt \varepsilon$ илиmath.isclose. - Хранить деньги в float. Копейки потеряются на округлениях. Для денег есть
decimal.Decimalили целые копейки. - Считать большие целые во float. Выше $2^{53}$ целые перестают быть точными; для них есть встроенный длинный
intPython.
Итоги
- Тип
float— это IEEE 754 double: знак, 11-битный порядок, 52-битная мантисса, $\approx$ 15–16 значащих цифр. - Представимые числа расположены неравномерно: точность относительная, у больших чисел «дырки» крупнее.
- Многие десятичные дроби ($0.1$, $0.2$) хранятся лишь приближённо — отсюда все «странности» float.
- Каждая операция округляется к ближайшему представимому; одна ошибка крошечна, опасно их накопление и усиление.