Плавающая точка IEEE 754: как компьютер хранит дроби
Урок объясняет внутреннее устройство типа float: формат IEEE 754, почему дробей «бесконечно много», а битов конечно, и что из этого следует.
IEEE 754 double — стандарт, по которому вещественное число хранится в 64 битах как знак (1 бит), порядок (11 бит) и мантисса (52 бита):
x = ±1.mantissa × 2^порядок.
Зачем вообще «плавающая» точка
Компьютеру нужно одним типом покрыть и заряд электрона (1.6e-19), и массу Солнца (2e30). Числа с фиксированной точкой (как у целых, но с запятой в фиксированном месте) для этого не годятся: либо не хватит дробной части для крошечных величин, либо целой — для гигантских. Решение — плавающая точка: храним значащие цифры (мантиссу) отдельно от масштаба (порядка), как в научной записи 1.6 × 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.2e-16, а около 10^16 — уже на 2. То есть при больших значениях целые числа перестают быть представимыми точно! 10^16 + 1 может равняться 10^16. Точность — относительная, а не абсолютная.
Двоичные дроби: почему 0.1 «некруглое»
В десятичной системе 1/3 = 0.333... — бесконечная дробь, её не записать точно конечным числом цифр. В двоичной системе ровно та же беда постигает 0.1: число 1/10 в двоичной записи периодично — 0.0001100110011... — и в 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 мантисса) |
| Значащих десятичных цифр | ≈ 15–16 |
| Диапазон по модулю | примерно 2.2e-308 … 1.8e+308 |
| Особые значения | inf, -inf, nan, -0.0 |
Частые ошибки
- Сравнивать float через
==. Из-за округления0.1 + 0.2 == 0.3ложно. Сравнивайте через допуск:abs(a − b) < epsилиmath.isclose. - Хранить деньги в float. Копейки потеряются на округлениях. Для денег есть
decimal.Decimalили целые копейки. - Считать большие целые во float. Выше
2^53целые перестают быть точными; для них есть встроенный длинныйintPython.
Итоги
- Тип
float— это IEEE 754 double: знак, 11-битный порядок, 52-битная мантисса, ≈ 15–16 значащих цифр. - Представимые числа расположены неравномерно: точность относительная, у больших чисел «дырки» крупнее.
- Многие десятичные дроби (
0.1,0.2) хранятся лишь приближённо — отсюда все «странности» float. - Каждая операция округляется к ближайшему представимому; одна ошибка крошечна, опасно их накопление и усиление.