В этой заметке мы рассмотрим, как числа с плавающей запятой хранятся в памяти. Напомню, что в прошлый раз мы говорили о нормализации, и это был второй этап (после приведения десятичной дроби к двоичной).

Нормализованную дробь нужно хранить в битовом контейнере и при этом выдержать несколько требований. Запись должна быть плотной, чтобы вместить больше информации. Борьба идет за каждый бит. В идеале запись должна упростить некоторые операции, например, чтобы мы проверяли только конкретные биты, а не все число.

Еще запись должна быть монотонной: накрутка битов как в целом числе должна монотонно увеличивать вещественное число.

Давайте запишем несколько нормализованных дробей:

 1.110011101 * 2^3
-1.010101 * 2^10
 1.001 * 2^6
-1.11101 * 2^100

Что между ними общего? Все их можно выразить тремя показателями:

  • был ли спереди минус;
  • степень двойки;
  • биты дробной части.

Возьмем наше любимое число -42.515625. В нормализованном виде оно равно:

-1.01010100001 * 2^5

Тройка его параметров выглядит так:

  • 1 (впереди минус);
  • 5 (степень);
  • 01010100001 (биты).

Вопрос: почему не учитывается целая часть? Та самая единица перед разделителем. Ответ – у нормализованной дроби целая часть всегда равна единице, поэтому она не хранится, а подразумевается. Имея тройку (1, 5, 01010100001), получим исходное число:

-1.01010100001 * 2^5

Далее тройка записывается в 32 бита (4 байта). Старший бит хранит знак, еще восемь бит – степень. На дробную часть остается 23 бита, она называется мантиссой. И напомню – целая часть дроби, равная единице, подразумевается.

Со степенью есть одна тонкость: она записывается со смещением 127 (половина размерности). Смысл в том, чтобы записать знаковое число как беззнаковое. Это необходимо, чтобы обеспечить монотонное возрастание числа при накрутке битов. Так, к степени 5 будет прибавлено 127 и получится 132. В двоичном виде это 10000100.

Итак, в битовом виде число -42.515625 равно:

1 10000100 01010100001000000000000

Пробелы разделяют смысловые части: знак, экспоненту с выравниванием, мантиссу. Битов 32, и они помещаются в 4 байта. Как с ними работать, рассмотрим позже.

Float определяет особые числа. Прежде всего это ноль, когда все биты нулевые. Особенность нуля в том, что его нельзя нормализовать. Помните, мы двигали разделитель, пока он не окажется за первым единичным битом? С нулем это невозможно – такого бита нет.

Еще одно интересное число – минус ноль:

1 00000000 00000000000000000000000

Минус ноль полезен в инженерных расчетах, например при сходимости рядов. Есть ряды, которые стремятся к нулю, но попеременно слева и справа. Минус ноль означает, что дальнейшие вычисления дают ноль и при этом мы были слева от нуля.

Другие интересные случаи:

  • экспонента 11111111, мантисса = 0 -> бесконечность (может быть отрицательной за счет первого бита)
  • экспонента 11111111, мантисса не равна 0 -> NaN
  • экспонента 0000000, мантисса не равна 0 -> субнормальные числа.

Субнормальные числа выровнены так, что экспонента равна нулю. Они поддерживаются процессорами, однако требуют внутренних преобразований.

Особые числа нельзя получить в лоб: чаще всего для этого возводят специальные флаги процессора, чтобы при NaN не возбуждалось исключение. Ну или у вас JavaScript.

В следующей заметке я планирую рассмотреть, как процессор складывает два числа с плавающей запятой.