Число, равное нулю, бесконечное или NaN, ясно, что это означает. Но это также говорит о ненормальности. Когда число является субнормальным?
Ответ 2
Основы IEEE 754
Сначала давайте рассмотрим основы IEEE 754 номера организованы.
Мы сосредоточимся на одинарной точности (32-битной), но все можно сразу обобщить на другие точности.
Формат:
- 1 бит: знак
- 8 бит: показатель степени
- 23 бита: дробь
Или, если вам нравятся картинки:
![enter image description here]()
Источник.
Знак прост: 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
в качестве первой цифры. E.g.:
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, как если бы у нас не было этого исключения)
Такие числа называются субнормальными числами (или ненормальными числами, которые являются синонимами).
Это правило сразу подразумевает, что число такое, что:
- показатель степени: 0
- фракция: 0
это 0.0
, что довольно элегантно, поскольку означает, что нужно следить за одним правилом меньше.
Таким образом, 0.0
на самом деле является ненормальным числом в соответствии с нашим определением!
Тогда с этим новым правилом наименьшее не субнормальное число:
- показатель степени: 1 (0 будет субнормальным)
- фракция: 0
который представляет:
1.0 * 2 ^ (-126)
Тогда самое большое субнормальное число:
- экспонента: 0
- фракция: 0x7FFFFF (23 бита 1)
что равно:
0.FFFFFE * 2 ^ (-126)
где .FFFFFE
еще раз - 23 бита на единицу справа от точки.
Это довольно близко к наименьшему не субнормальному числу, которое звучит вменяемым.
И наименьшее ненулевое субнормальное число:
что равно:
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 одинарной точности.
В частности, это касается моего ноутбука 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 "Сброс в ноль" описывает конфигурируемый режим, в котором субнормалы округляются до нуля для повышения производительности:
Производительность обработки с плавающей запятой может быть снижена при выполнении вычислений, включающих денормализованные числа и исключения Underflow. Во многих алгоритмах эту производительность можно восстановить, не оказывая существенного влияния на точность конечного результата, заменив денормализованные операнды и промежуточные результаты нулями. Чтобы разрешить эту оптимизацию, реализации ARM с плавающей точкой позволяют использовать режим Flush-to-zero для различных форматов с плавающей точкой следующим образом:
-
For AArch64:
Если FPCR.FZ==1
, то режим Flush-to-Zero используется для всех входов и выходов одинарной и двойной точности всех инструкций.
If FPCR.FZ16==1
, then Flush-to-Zero mode is used for all Half-Precision inputs и outputs of floating-point instructions, other than:—Conversions between Half-Precision и Single-Precision numbers.—Conversions between Half-Precision и Double-Precision numbers.
A1.5.2 "Стандарты с плавающей точкой и терминология" Таблица A1-3 "Терминология с плавающей точкой" подтверждает, что субнормалы и денормалы являются синонимами:
This manual IEEE 754-2008
------------------------- -------------
[...]
Denormal, or denormalized Subnormal
C5.2.7 "FPCR, регистр управления с плавающей запятой" описывает, как ARMv8 может необязательно вызывать исключения или устанавливать биты флага, когда вход операции с плавающей запятой является ненормальным:
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, bits [3:0]
Переход в нулевой режим. Указывает, обеспечивает ли реализация с плавающей запятой поддержку только для режима работы Flush-to-Zero. Определены значения:
0b0000 Не реализовано, или аппаратное обеспечение поддерживает только режим работы "Flush-to-Zero".
0b0001 Аппаратное обеспечение поддерживает арифметику полного денормализованного числа.
Все остальные значения зарезервированы.
В ARMv8-A разрешенными значениями являются 0b0000 и 0b0001.
Это говорит о том, что когда субнормалы не реализованы, реализации просто возвращаются к нулю.
Бесконечность и NaN
Любопытно? Я написал кое-что по адресу: