Фиксированная и плавающая точка
Разбираемся, как из конечного набора бит компьютер собирает вещественное число — через знак, мантиссу и порядок, по стандарту IEEE 754.
Плавающая точка — способ хранить вещественное число в виде «мантисса, умноженная на основание в степени порядка», где запятая не закреплена на месте, а «плавает» в зависимости от порядка.
В прошлом уроке мы выяснили, что дробь занимает биты после запятой. Но где компьютеру поставить саму запятую? Есть два подхода. Фиксированная точка жёстко закрепляет, сколько бит идёт под целую часть и сколько под дробную. Плавающая точка позволяет запятой двигаться, что даёт колоссальный диапазон значений при том же числе бит. Именно плавающая точка (тип float и double) используется почти везде, и понимать её устройство нужно, чтобы не удивляться странностям вроде разных результатов у одинаковых на вид вычислений.
Фиксированная точка
Идея проста: договариваемся, что, скажем, в 16-битном числе старшие 8 бит — целая часть, младшие 8 бит — дробная. Тогда комбинация бит 00000110.01000000 читается как $6{,}25$. Шаг (точность) такого представления постоянен и равен $\frac{1}{2^8} = \frac{1}{256} \approx 0{,}0039$ на всём диапазоне.
- Плюсы: простая и быстрая арифметика (по сути целочисленная), предсказуемая точность. Используется в финансах, DSP, микроконтроллерах без FPU.
- Минусы: узкий диапазон. С 8 битами на целую часть максимум — около $255$, а очень маленькие числа вроде $0{,}000001$ просто не помещаются — не хватает дробных разрядов.
Плавающая точка: знак, мантисса, порядок
Чтобы охватить и гигантские, и крошечные числа, применяют научную форму записи. В десятичной мы пишем $6{,}022 \cdot 10^{23}$ или $1{,}6 \cdot 10^{-19}$. Компьютер делает то же, но в двоичной системе. Любое вещественное число раскладывается на три компонента:
- Знак $s$ — один бит: $0$ для плюса, $1$ для минуса.
- Мантисса (significand) $M$ — значащие цифры числа, например $1{,}625$.
- Порядок (экспонента) $E$ — на сколько разрядов сдвинуть запятую, то есть в какую степень возвести двойку.
Значение собирается по главной формуле плавающей точки:
$$x = (-1)^s \cdot M \cdot 2^{E}$$
Запятая «плавает»: меняя порядок $E$, мы двигаем её влево или вправо. Для больших чисел $E$ велик и положителен, для малых — отрицателен. Поэтому одним и тем же числом бит покрывается диапазон от $10^{-38}$ до $10^{38}$ (для 32-битного float).
Как это работает: стандарт IEEE 754
Чтобы все процессоры считали одинаково, договорились о стандарте IEEE 754. Он фиксирует, сколько бит отвести каждому компоненту:
| Тип | Всего бит | Знак | Порядок | Мантисса | Точность |
|---|---|---|---|---|---|
float (одинарная) | 32 | 1 | 8 | 23 | ~7 значащих цифр |
double (двойная) | 64 | 1 | 11 | 52 | ~15–16 значащих цифр |
Зубрить раскладку бит не нужно, но важны два инженерных приёма стандарта:
Нормализация и скрытый бит
Мантиссу всегда нормализуют так, чтобы перед запятой стояла ровно одна значащая цифра — в двоичной это всегда $1$ (нулём она быть не может, иначе сдвинули бы порядок). Раз эта единица всегда там, её не хранят — она «скрытая». Поэтому реальная мантисса равна $M = 1{,}f$, где $f$ — биты, записанные в поле мантиссы. Это бесплатно добавляет один бит точности.
Смещённый порядок
Порядок $E$ бывает и отрицательным (для малых чисел), но хранить знак отдельно неудобно. Поэтому в поле порядка пишут смещённое значение $E_{\text{store}} = E + \text{bias}$, где для double $\text{bias} = 1023$. Чтобы получить настоящий порядок, из хранимого вычитают смещение: $E = E_{\text{store}} - 1023$.
Соберём всё вместе и проверим формулу на конкретном числе $6{,}5$. Распишем его по компонентам и восстановим обратно:
import struct
def ieee_parts(value):
# упаковываем double в 64 бита и вытаскиваем поля
[bits] = struct.unpack('>Q', struct.pack('>d', value))
s = bits >> 63 # бит знака
e_stored = (bits >> 52) & 0x7FF # 11 бит порядка
frac = bits & ((1 << 52) - 1) # 52 бита мантиссы
E = e_stored - 1023 # убираем смещение
M = 1 + frac / (2 ** 52) # добавляем скрытую единицу
return s, E, M
for v in (6.5, 0.5, -3.0):
s, E, M = ieee_parts(v)
recon = ((-1) ** s) * M * (2 ** E) # формула (-1)^s * M * 2^E
print(v, "-> s=%d, E=%d, M=%.4f, recon=%g" % (s, E, M, recon))
Вывод:
6.5 -> s=0, E=2, M=1.6250, recon=6.5 0.5 -> s=0, E=-1, M=1.0000, recon=0.5 -3.0 -> s=1, E=1, M=1.5000, recon=-3
Разберём первую строку. Число $6{,}5$ в двоичном виде равно $110{,}1_2$. Нормализуем — сдвигаем запятую на два разряда влево: $1{,}101_2 \cdot 2^{2}$. Значит знак $s = 0$, порядок $E = 2$, мантисса $M = 1{,}101_2 = 1{,}625$. Подставляем в формулу: $(-1)^0 \cdot 1{,}625 \cdot 2^2 = 1{,}625 \cdot 4 = 6{,}5$. Сошлось в точности, потому что $6{,}5 = \frac{13}{2}$ — дробь со знаменателем-степенью двойки.
Сравнение фиксированной и плавающей точки
| Свойство | Фиксированная точка | Плавающая точка (IEEE 754) |
|---|---|---|
| Диапазон | Узкий, задан заранее | Огромный (от $10^{-308}$ до $10^{308}$ для double) |
| Абсолютная точность | Постоянна на всём диапазоне | Относительна: меняется с величиной числа |
| Скорость | Как у целых, очень быстро | Нужен FPU, чуть медленнее |
| Где применяют | Деньги, DSP, микроконтроллеры | Научные расчёты, графика, общий код |
Главное отличие — характер точности. У фиксированной точки шаг между соседними значениями всегда одинаков. У плавающей он относительный: рядом с $1$ числа идут очень густо, а рядом с $1\,000\,000$ соседние представимые значения отстоят друг от друга уже на заметную величину. Поэтому плавающая точка отлично хранит и микроскопические, и астрономические величины, но почти никогда — абсолютно точно.
Частые ошибки
- Думают, что double хранит числа точно. Нет: точно хранятся лишь дроби со знаменателем — степенью двойки (и небольшие целые). $0{,}1$ или $0{,}3$ хранятся приближённо.
- Путают мантиссу и порядок. Мантисса задаёт значащие цифры, порядок — положение запятой. Меняется порядок — число масштабируется в разы, а не на единицы.
- Забывают про скрытый бит. Реальная мантисса — это $1{,}f$, а не просто записанные биты $f$; ведущая единица подразумевается.
- Считают точность абсолютной. У float ~7 значащих цифр — это относительная точность; для больших чисел абсолютная погрешность велика.
Итоги
- Фиксированная точка закрепляет положение запятой: простая и быстрая, но с узким диапазоном.
- Плавающая точка хранит число как $(-1)^s \cdot M \cdot 2^{E}$ — знак, мантисса, порядок — и охватывает огромный диапазон.
- Стандарт IEEE 754 задаёт раскладку бит:
float(32 бита, ~7 цифр) иdouble(64 бита, ~15–16 цифр), со скрытым битом мантиссы и смещённым порядком. - Точность плавающей точки относительна: чем больше число, тем грубее шаг между соседними представимыми значениями.