Основы 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
, что выглядит неплохо.
Если рассматривать только эти правила, то наименьшее ненулевое число, которое можно представитьбудет:
, что выглядит примерно так в шестнадцатеричной дроби из-за соглашения о ведущих битах:
1.000002 * 2 ^ (-127)
, где .000002
- 22 нуля с 1
в конце.
Мы не можем взять fraction = 0
, иначе это число будет 0.0
.
Но тогда инженеры, которые также имели острый артистический смысл, подумали: разве это не уродливо?Что мы прыгаем с прямой 0.0
до чего-то, что даже не является степенью 2?Разве мы не можем представить даже меньшие числа?
Субнормальные числа
Инженеры на некоторое время почесали головы и, как обычно, вернулись с еще одной хорошей идеей,Что если мы создадим новое правило:
Если показатель степени равен 0, то:
- старший бит становится 0
- , показатель степени фиксируется на-126 (не -127, как если бы у нас не было этого исключения)
Такие числа называются субнормальными числами (или ненормальными числами, которые являются синонимами).
ThisПравило сразу подразумевает, что число такое, что:
равно 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
Теперь давайте поиграем с реальным кодом, чтобы проверить наш тheory.
Почти на всех современных и настольных компьютерах C float
представляет числа с плавающей запятой IEEE 754 одинарной точности.
Это, в частности, случай с моим ноутбуком Ubuntu 18.04 amd64 Lenovo P51.
При таком предположении все утверждения передаются следующей программе:
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) {
/* Basic examples. */
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));
/* Quick review of C hex floating point literals. */
assert(0.5f == 0x1.0p-1f);
assert(1.0f == 0x1.0p0f);
assert(2.0f == 0x1.0p1f);
/* Sign bit. */
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));
/* The special case of 0.0 and -0.0. */
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);
/* ANSI C defines FLT_MIN as the smallest non-subnormal number. */
assert(FLT_MIN == 0x1.0p-126f);
assert(float32_equal(FLT_MIN, 0, 1, 0));
assert(isnormal(FLT_MIN));
/* The largest subnormal number. */
float largest_subnormal = float_from_bytes(0, 0, 0x7FFFFF);
assert(largest_subnormal == 0x0.FFFFFEp-126f);
assert(largest_subnormal < FLT_MIN);
assert(!isnormal(largest_subnormal));
/* The smallest non-zero subnormal number. */
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 upstream .
Скомпилируйте и запустите с:
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 Руководство A1.5.4 «Сброс в ноль» описывает настраиваемый режим, в котором субнормалы округляются доноль для улучшения производительности:
Производительность обработки с плавающей запятой может быть снижена при выполнении вычислений с использованием денормализованных чисел и исключений из-за недостаточного значения.Во многих алгоритмах эту производительность можно восстановить, не оказывая существенного влияния на точность конечного результата, заменив денормализованные операнды и промежуточные результаты нулями.Чтобы разрешить эту оптимизацию, реализации ARM с плавающей точкой позволяют использовать режим Flush-to-zero для различных форматов с плавающей точкой следующим образом:
Для AArch64:
Если FPCR.FZ==1
, то режим Flush-to-Zero используется для всех входов и выходов одинарной и двойной точности всех инструкций.
ЕслиFPCR.FZ16==1
, затем режим Flush-to-Zero используется для всех входов и выходов Half-Precision команд с плавающей запятой, кроме: - Преобразования между числами Half-Precision и Single-Precision. - Преобразования между Half-Precision и Double-Точности.
A1.5.2 «Стандарты с плавающей точкой и терминология» Таблица A1-3 «Терминология с плавающей точкой» подтверждает, что субнормальныеа денормалы являются синонимами:
This manual IEEE 754-2008
------------------------- -------------
[...]
Denormal, or denormalized Subnormal
C5.2.7 «FPCR, регистр управления с плавающей запятой» описывает, как ARMv8 может необязательно вызывать исключения или устанавливать биты флага всякий раз, когда вводится floatiОперация ng point является ненормальной:
FPCR.IDE, bit [15] Вход Денормалировано включение ловушки исключения с плавающей точкой.Возможные значения:
0b0 Выбрана необработанная обработка исключений.Если возникает исключение с плавающей запятой, то бит FPSR.IDC устанавливается в 1.
0b1 Выбрана обработка исключений в ловушке.Если возникает исключение с плавающей точкой, PE не обновляет бит FPSR.IDC.Программное обеспечение обработки прерываний может решить, установить ли бит FPSR.IDC в 1.
D12.2.88 "MVFR1_EL1, AArch32 Media и VFP Feature Register 1" показывает, что ненормальная поддержкана самом деле является полностью необязательным и предлагает бит для определения наличия поддержки:
FPFtZ, биты [3: 0]
Flush to Zero mode.Указывает, обеспечивает ли реализация с плавающей запятой поддержку только для режима работы Flush-to-Zero.Определены следующие значения:
0b0000 Не реализовано, или аппаратное обеспечение поддерживает только режим работы с заполнением нулями.
0b0001 Аппаратное обеспечение поддерживает полноеарифметика денормализованного числа.
Все остальные значения зарезервированы.
В ARMv8-A допустимые значения: 0b0000 и 0b0001.
Это говорит о том, что когда субнормалы не реализованы, реализации просто возвращаются к нулю.
Бесконечность и NaN
Любопытно?Я написал несколько вещей по адресу: