Основи 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)
де .000002
22 нулі з 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) короткий реальний світ історичний огляд у вигляді інтерв'ю з Кехен було запропоновано Джона Колемана в коментарях.