Фиксированная и плавающая точка

Разбираемся, как из конечного набора бит компьютер собирает вещественное число — через знак, мантиссу и порядок, по стандарту 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 (одинарная)321823~7 значащих цифр
double (двойная)6411152~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 цифр), со скрытым битом мантиссы и смещённым порядком.
  • Точность плавающей точки относительна: чем больше число, тем грубее шаг между соседними представимыми значениями.
Проверьте себя
1. По какой формуле IEEE 754 восстанавливает значение числа из его полей?
Ax = s + M + E
Bx = (-1)^s · M · 2^E, где s — знак, M — мантисса, E — порядок
Cx = M / 2^E
Dx = (-1)^E · M · 2^s
2. Что такое «скрытый бит» мантиссы в нормализованном числе IEEE 754?
AБит знака, который не отображается
BВедущая единица перед запятой (1.f), которая всегда есть и потому не хранится
CПоследний бит порядка
DБит чётности для контроля ошибок
3. Чем точность плавающей точки отличается от фиксированной?
AУ плавающей точки точность всегда выше
BУ фиксированной шаг постоянен, у плавающей он относителен — растёт вместе с величиной числа
CУ плавающей точки нет погрешностей вообще
DОни абсолютно одинаковы