Основы IEEE 754
Сначала давайте рассмотрим основы организации номеров IEEE 754.
Мы сосредоточимся на одинарной точности (32 бита), но все можно сразу же обобщить на другие точности.
Формат:
- 1 бит: знак
- 8 бит: показатель степени
- 23 бита: дробь
Или, если вам нравятся картинки:

Источник .
Знак простой: 0 - положительный, а 1 - отрицательный, конец истории.
Показатель экспоненты составляет 8 бит, поэтому он колеблется от 0 до 255.
Показатель степени называется смещенным, потому что он имеет смещение -127, например:
0 == special case: zero or subnormal, explained below
1 == 2 ^ -126
...
125 == 2 ^ -2
126 == 2 ^ -1
127 == 2 ^ 0
128 == 2 ^ 1
129 == 2 ^ 2
...
254 == 2 ^ 127
255 == special case: infinity and NaN
Соглашение о ведущих битах
(Далее следует вымышленный гипотетический рассказ, не основанный на каких-либо реальных исторических исследованиях.)
При разработке стандарта IEEE 754 инженеры заметили, что все числа, за исключением 0.0, имеют 1двоичную единицу в качестве первой цифры. Например:
25.0 == (binary) 11001 == 1.1001 * 2^4
0.625 == (binary) 0.101 == 1.01 * 2^-1
оба начинаются с этой надоедливой 1.части.
Поэтому было бы расточительным позволять этой цифре занимать один бит точности почти для каждого отдельного числа.
По этой причине они создали «соглашение о начальных битах»:
всегда предполагайте, что число начинается с единицы
Но как тогда бороться 0.0? Что ж, решили создать исключение:
- если показатель равен 0
- а дробь равна 0
- тогда число представляет плюс или минус
0.0
так что байты 00 00 00 00также представляют 0.0, что выглядит хорошо.
Если бы мы учитывали только эти правила, то наименьшее ненулевое число, которое может быть представлено, было бы:
- показатель степени: 0
- фракция: 1
что выглядит примерно так в шестнадцатеричной дроби из-за соглашения о начальных битах:
1.000002 * 2 ^ (-127)
где .00000222 нуля с буквой 1на конце.
Мы не можем взять fraction = 0, иначе было бы это число 0.0.
Но потом инженеры, имевшие к тому же острое эстетическое чутье, подумали: разве это не уродливо? Что мы прыгаем прямо0.0 к чему-то, что даже не является истинной степенью двойки? Не могли бы мы как-нибудь представить еще меньшие числа? (Хорошо, это было немного больше, чем «уродливое»: на самом деле люди получали плохие результаты своих вычислений, см. «Как субнормальные факторы улучшают вычисления» ниже).
Субнормальные числа
Инженеры немного почесали затылки и, как обычно, вернулись с другой хорошей идеей. Что, если мы создадим новое правило:
Если показатель степени равен 0, то:
- ведущий бит становится 0
- экспонента фиксируется на -126 (не -127, как если бы у нас не было этого исключения)
Такие числа называются субнормальными числами (или денормальными числами, что является синонимом).
Из этого правила сразу следует, что число такое, что:
- показатель степени: 0
- фракция: 0
по-прежнему 0.0, что довольно элегантно, поскольку означает, что на одно правило меньше.
Так 0.0на самом деле субнормальный номер в соответствии с нашим определением!
Таким образом, с помощью этого нового правила наименьшее ненормальное число будет:
- показатель степени: 1 (0 будет субнормальным)
- фракция: 0
что представляет собой:
1.0 * 2 ^ (-126)
Тогда наибольшее субнормальное число:
- показатель степени: 0
- дробь: 0x7FFFFF (23 бита 1)
что равно:
0.FFFFFE * 2 ^ (-126)
где .FFFFFEснова 23 бита, единица справа от точки.
Это довольно близко к наименьшему ненормальному числу, что звучит нормально.
И наименьшее ненулевое субнормальное число:
- показатель степени: 0
- фракция: 1
что равно:
0.000002 * 2 ^ (-126)
что тоже выглядит довольно близко к 0.0!
Не сумев найти какой-либо разумный способ представить числа меньшие, чем это, инженеры были счастливы и вернулись к просмотру картинок кошек в Интернете или что-то еще, что они делали в 70-х.
Как видите, субнормальные числа являются компромиссом между точностью и длиной представления.
В качестве самого крайнего примера, наименьшее ненулевое субнормальное:
0.000002 * 2 ^ (-126)
по существу имеет точность одного бита вместо 32 бита. Например, если мы разделим его на два:
0.000002 * 2 ^ (-126) / 2
мы реально достигаем 0.0ровно!
Визуализация
Всегда полезно иметь геометрическую интуицию относительно того, что мы изучаем, так что начнем.
Если мы нанесем числа с плавающей запятой IEEE 754 на линию для каждого заданного показателя степени, это будет выглядеть примерно так:
+---+-------+---------------+-------------------------------+
exponent |126| 127 | 128 | 129 |
+---+-------+---------------+-------------------------------+
| | | | |
v v v v v
-------------------------------------------------------------
floats ***** * * * * * * * * * * * *
-------------------------------------------------------------
^ ^ ^ ^ ^
| | | | |
0.5 1.0 2.0 4.0 8.0
Из этого мы видим, что:
- для каждой экспоненты нет перекрытия между представленными числами
- для каждого показателя у нас есть одно и то же число 2 ^ 32 чисел (здесь представлено 4
*)
- внутри каждой экспоненты точки расположены на одинаковом расстоянии
- более крупные показатели охватывают более крупные диапазоны, но с более разбросанными точками
Теперь давайте полностью опустим это до степени 0.
Без субнормальных явлений это гипотетически выглядело бы так:
+---+---+-------+---------------+-------------------------------+
exponent | ? | 0 | 1 | 2 | 3 |
+---+---+-------+---------------+-------------------------------+
| | | | | |
v v v v v v
-----------------------------------------------------------------
floats * **** * * * * * * * * * * * *
-----------------------------------------------------------------
^ ^ ^ ^ ^ ^
| | | | | |
0 | 2^-126 2^-125 2^-124 2^-123
|
2^-127
С субнормальными это выглядит так:
+-------+-------+---------------+-------------------------------+
exponent | 0 | 1 | 2 | 3 |
+-------+-------+---------------+-------------------------------+
| | | | |
v v v v v
-----------------------------------------------------------------
floats * * * * * * * * * * * * * * * * *
-----------------------------------------------------------------
^ ^ ^ ^ ^ ^
| | | | | |
0 | 2^-126 2^-125 2^-124 2^-123
|
2^-127
Сравнивая два графика, мы видим, что:
субнормальные удваивают длину диапазона экспоненты 0, от [2^-127, 2^-126)до[0, 2^-126)
Расстояние между числами с плавающей запятой в субнормальном диапазоне такое же, как и для [0, 2^-126).
диапазон [2^-127, 2^-126)имеет половину количества точек, которое он имел бы без субнормальных значений.
Половина этих баллов идет на заполнение другой половины диапазона.
в диапазоне [0, 2^-127)есть некоторые точки с субнормальными значениями, но без них нет.
Это отсутствие очков [0, 2^-127)не очень элегантно и является основной причиной существования субнормальных существ!
поскольку точки расположены на одинаковом расстоянии:
- диапазон
[2^-128, 2^-127)имеет половину баллов, чем [2^-127, 2^-126)
- [2^-129, 2^-128)имеет половину баллов, чем[2^-128, 2^-127)
- и так далее
Это то, что мы имеем в виду, когда говорим, что субнормальные значения - это компромисс между размером и точностью.
Пример исполняемого C
Теперь давайте поиграемся с реальным кодом, чтобы проверить нашу теорию.
Практически на всех современных и настольных компьютерах C floatпредставляет числа с плавающей запятой одинарной точности IEEE 754.
В частности, это относится к моему ноутбуку Lenovo P51 с Ubuntu 18.04 amd64.
При таком предположении все утверждения передаются следующей программе:
subnormal.c
#if __STDC_VERSION__ < 201112L
#error C11 required
#endif
#ifndef __STDC_IEC_559__
#error IEEE 754 not implemented
#endif
#include <assert.h>
#include <float.h> /* FLT_HAS_SUBNORM */
#include <inttypes.h>
#include <math.h> /* isnormal */
#include <stdlib.h>
#include <stdio.h>
#if FLT_HAS_SUBNORM != 1
#error float does not have subnormal numbers
#endif
typedef struct {
uint32_t sign, exponent, fraction;
} Float32;
Float32 float32_from_float(float f) {
uint32_t bytes;
Float32 float32;
bytes = *(uint32_t*)&f;
float32.fraction = bytes & 0x007FFFFF;
bytes >>= 23;
float32.exponent = bytes & 0x000000FF;
bytes >>= 8;
float32.sign = bytes & 0x000000001;
bytes >>= 1;
return float32;
}
float float_from_bytes(
uint32_t sign,
uint32_t exponent,
uint32_t fraction
) {
uint32_t bytes;
bytes = 0;
bytes |= sign;
bytes <<= 8;
bytes |= exponent;
bytes <<= 23;
bytes |= fraction;
return *(float*)&bytes;
}
int float32_equal(
float f,
uint32_t sign,
uint32_t exponent,
uint32_t fraction
) {
Float32 float32;
float32 = float32_from_float(f);
return
(float32.sign == sign) &&
(float32.exponent == exponent) &&
(float32.fraction == fraction)
;
}
void float32_print(float f) {
Float32 float32 = float32_from_float(f);
printf(
"%" PRIu32 " %" PRIu32 " %" PRIu32 "\n",
float32.sign, float32.exponent, float32.fraction
);
}
int main(void) {
assert(float32_equal(0.5f, 0, 126, 0));
assert(float32_equal(1.0f, 0, 127, 0));
assert(float32_equal(2.0f, 0, 128, 0));
assert(isnormal(0.5f));
assert(isnormal(1.0f));
assert(isnormal(2.0f));
assert(0.5f == 0x1.0p-1f);
assert(1.0f == 0x1.0p0f);
assert(2.0f == 0x1.0p1f);
assert(float32_equal(-0.5f, 1, 126, 0));
assert(float32_equal(-1.0f, 1, 127, 0));
assert(float32_equal(-2.0f, 1, 128, 0));
assert(isnormal(-0.5f));
assert(isnormal(-1.0f));
assert(isnormal(-2.0f));
assert(float32_equal( 0.0f, 0, 0, 0));
assert(float32_equal(-0.0f, 1, 0, 0));
assert(!isnormal( 0.0f));
assert(!isnormal(-0.0f));
assert(0.0f == -0.0f);
assert(FLT_MIN == 0x1.0p-126f);
assert(float32_equal(FLT_MIN, 0, 1, 0));
assert(isnormal(FLT_MIN));
float largest_subnormal = float_from_bytes(0, 0, 0x7FFFFF);
assert(largest_subnormal == 0x0.FFFFFEp-126f);
assert(largest_subnormal < FLT_MIN);
assert(!isnormal(largest_subnormal));
float smallest_subnormal = float_from_bytes(0, 0, 1);
assert(smallest_subnormal == 0x0.000002p-126f);
assert(0.0f < smallest_subnormal);
assert(!isnormal(smallest_subnormal));
return EXIT_SUCCESS;
}
GitHub вверх по течению .
Скомпилируйте и запустите с:
gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c
./subnormal.out
C ++
В дополнение к раскрытию всех API C, C ++ также предоставляет некоторые дополнительные субнормальные функции, которые не так легко доступны в C <limits>, например:
denorm_min: Возвращает минимальное положительное субнормальное значение типа T
В C ++ весь API шаблонизирован для каждого типа с плавающей запятой, и это намного лучше.
Реализации
x86_64 и ARMv8 реализуют IEEE 754 непосредственно на оборудовании, в которое преобразуется код C.
В некоторых реализациях субнормалы кажутся менее быстрыми, чем нормальные: почему изменение 0,1f на 0 снижает производительность в 10 раз? Это упоминается в руководстве по ARM, см. Раздел «Подробности ARMv8» в этом ответе.
ARMv8 подробности
Справочное руководство по архитектуре ARM ARMv8 DDI 0487C.a manual A1.5.4 "Flush-to-zero" описывает настраиваемый режим, в котором субнормальные значения округляются до нуля для повышения производительности:
Производительность обработки с плавающей запятой может быть снижена при выполнении вычислений с использованием денормализованных чисел и исключений потери значимости. Во многих алгоритмах эту производительность можно восстановить без значительного влияния на точность конечного результата, заменив денормализованные операнды и промежуточные результаты нулями. Чтобы разрешить эту оптимизацию, реализации с плавающей запятой ARM позволяют использовать режим Flush-to-zero для различных форматов с плавающей запятой следующим образом:
Для AArch64:
Если FPCR.FZ==1, то режим Flush-to-Zero используется для всех входов и выходов одинарной и двойной точности всех инструкций.
Если FPCR.FZ16==1, то режим Flush-to-Zero используется для всех входов и выходов половинной точности инструкций с плавающей запятой, кроме: - Преобразования между числами половинной точности и одинарной точности. - Преобразования между половинной точностью и двойной точностью. числа.
A1.5.2 «Стандарты с плавающей запятой и терминология» Таблица A1-3 «Терминология с плавающей запятой» подтверждает, что субнормальные и денормальные числа являются синонимами:
This manual IEEE 754-2008
------------------------- -------------
[...]
Denormal, or denormalized Subnormal
C5.2.7 «FPCR, регистр управления с плавающей запятой» описывает, как ARMv8 может опционально вызывать исключения или устанавливать биты флага всякий раз, когда ввод операции с плавающей запятой является субнормальным:
FPCR.IDE, бит [15] Входное разрешение прерывания исключительной ситуации с ненормальной плавающей точкой. Возможные значения:
0b0 Выбрана неотработанная обработка исключений. Если возникает исключительная ситуация с плавающей запятой, бит FPSR.IDC устанавливается в 1.
0b1 Выбрана обработка захваченного исключения. Если возникает исключительная ситуация с плавающей запятой, PE не обновляет бит FPSR.IDC. Программа обработки прерываний может решить, устанавливать ли бит FPSR.IDC в 1.
D12.2.88 «MVFR1_EL1, AArch32 Media and VFP Feature Register 1» показывает, что денормальная поддержка фактически не является обязательной, и предлагает немного определить, есть ли поддержка:
FPFtZ, биты [3: 0]
Сброс до нулевого режима. Указывает, поддерживает ли реализация с плавающей запятой только режим работы Flush-to-Zero. Определенные значения:
0b0000 Не реализовано, или оборудование поддерживает только режим работы Flush-to-Zero.
0b0001 Аппаратное обеспечение поддерживает полную арифметику денормализованных чисел.
Все остальные значения зарезервированы.
В ARMv8-A допустимые значения - 0b0000 и 0b0001.
Это говорит о том, что когда субнормальные функции не реализованы, реализации просто возвращаются к нулевому значению.
Бесконечность и NaN
Любопытно? Я писал кое-что по адресу:
Как субнормальные факторы улучшают вычисления
TODO: дополнительно понять, как этот скачок ухудшает результаты вычислений / как субнормальные значения улучшают результаты вычислений.
Актуальная история
Чарльз Северанс « Интервью со стариком с плавающей точкой » (1998) представляет собой краткий исторический обзор реального мира в форме интервью с Уильямом Каханом, предложенный Джоном Коулманом в комментариях.