Основи 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 нулі з a 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
- дріб: 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.
Це зокрема стосується мого ноутбука Ubuntu 18.04 amd64 Lenovo P51.
З цим припущенням усі твердження передаються за такою програмою:
субнормальний. 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 безпосередньо на апаратному забезпеченні, на що перекладається код С.
У деяких реалізаціях субнормальні показники є менш швидкими, ніж нормальні: Чому зміна 0,1f на 0 сповільнює продуктивність в 10 разів? Про це йдеться у посібнику ARM, див. Розділ "Подробиці ARMv8" цієї відповіді.
Подробиці ARMv8
Довідковий посібник з ARM-архітектури ARMv8 DDI 0487C. посібник A1.5.4 "Змив до нуля" описує настроюваний режим, коли субнормалі округляються до нуля для поліпшення продуктивності:
Ефективність обробки з плаваючою точкою може бути знижена при виконанні обчислень із денормалізованими числами та винятками Underflow. У багатьох алгоритмах цю продуктивність можна відновити, не суттєво впливаючи на точність кінцевого результату, замінюючи денормалізовані операнди та проміжні результати нулями. Щоб дозволити цю оптимізацію, реалізації з плаваючою крапкою ARM дозволяють використовувати режим змиву до нуля для різних форматів з плаваючою точкою, як показано нижче:
Для AArch64:
Якщо FPCR.FZ==1, тоді режим «Змив-нуль» використовується для всіх входів та виходів з однією точністю та подвійною точністю всіх інструкцій.
Якщо FPCR.FZ16==1, тоді режим Flush-to-Zero використовується для всіх входів та виходів напівточної точності з інструкціями з плаваючою комою, крім: - Перетворення між числами Напівточності та Одноточної точності. - Перетворення між Напівточністю та Подвійною точністю числа.
А1.5.2 "Стандарти з плаваючою крапкою та термінологія" Таблиця А1-3 "Термінологія з плаваючою крапкою" підтверджує, що субнормалі та денормали є синонімами:
This manual IEEE 754-2008
------------------------- -------------
[...]
Denormal, or denormalized Subnormal
C5.2.7 "FPCR, реєстр керування плаваючою точкою" описує, як ARMv8 може додатково створювати винятки або встановлювати біти прапора, коли вхід операції з плаваючою точкою є ненормальним:
FPCR.IDE, біт [15] Увімкнути панораму виключення з плаваючою комою в режимі Denormal. Можливі значення:
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. Визначені значення:
0b0000 Не реалізовано, або апаратне забезпечення підтримує лише режим роботи Flush-to-Zero.
0b0001 Апаратне забезпечення підтримує повну денормалізовану арифметику чисел.
Усі інші значення зарезервовані.
В ARMv8-A дозволеними значеннями є 0b0000 та 0b0001.
Це свідчить про те, що коли субнормалі не реалізовані, реалізації просто повертаються до рівня нуля.
Нескінченність і NaN
Цікаво? Я писав деякі речі за адресою:
Як субнормальні покращують обчислення
TODO: далі зрозуміти більш точно, як цей стрибок погіршує результати обчислення / як субнормальні покращують результати обчислення.
Фактична історія
Інтерв'ю з Старшого з плаваючою точкою по Чарльз Severance . (1998) короткий реальний світ історичний огляд у вигляді інтерв'ю з Кехен було запропоновано Джона Колемана в коментарях.