Podstawy IEEE 754
Najpierw przyjrzyjmy się podstawom uporządkowania numerów IEEE 754.
Skoncentrujemy się na pojedynczej precyzji (32-bitowej), ale wszystko można natychmiast uogólnić na inne dokładności.
Format to:
- 1 bit: znak
- 8 bitów: wykładnik
- 23 bity: ułamek
Lub jeśli lubisz zdjęcia:
Źródło .
Znak jest prosty: 0 jest pozytywne, a 1 jest negatywne, koniec historii.
Wykładnik ma długość 8 bitów, a więc zawiera się w zakresie od 0 do 255.
Wykładnik nazywany jest obciążeniem, ponieważ ma przesunięcie -127
, np .:
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
Wiodąca konwencja bitów
(Poniżej przedstawiono fikcyjną, hipotetyczną narrację, nieopartą na żadnych faktycznych badaniach historycznych).
Podczas projektowania IEEE 754 inżynierowie zauważyli, że wszystkie liczby, z wyjątkiem 0.0
, mają 1
jedynkę w systemie dwójkowym jako pierwszą cyfrę. Na przykład:
25.0 == (binary) 11001 == 1.1001 * 2^4
0.625 == (binary) 0.101 == 1.01 * 2^-1
oba zaczynają się od tego irytującego 1.
części.
Dlatego marnotrawstwem byłoby pozwolić, aby ta cyfra zajmowała jeden precyzyjny bit prawie każdej liczby.
Z tego powodu stworzyli „wiodącą konwencję bitową”:
zawsze zakładaj, że liczba zaczyna się od jedynki
Ale jak sobie z tym radzić 0.0
? Cóż, postanowili stworzyć wyjątek:
- jeśli wykładnik wynosi 0
- a ułamek to 0
- to liczba oznacza plus lub minus
0.0
tak, że bajty 00 00 00 00
również reprezentują 0.0
, co wygląda dobrze.
Gdybyśmy tylko rozważyli te reguły, najmniejsza niezerowa liczba, którą można przedstawić, to:
który wygląda mniej więcej tak w ułamku szesnastkowym z powodu wiodącej konwencji bitów:
1.000002 * 2 ^ (-127)
gdzie .000002
jest 22 zera i 1
na końcu.
Nie możemy wziąć fraction = 0
, inaczej byłaby ta liczba 0.0
.
Ale wtedy inżynierowie, którzy również mieli wyostrzony zmysł estetyczny, pomyśleli: czy to nie jest brzydkie? Że skaczemy od razu 0.0
do czegoś, co nie ma nawet właściwej potęgi 2? Czy nie moglibyśmy w jakiś sposób przedstawić nawet mniejszych liczb? (OK, to było trochę bardziej niepokojące niż „brzydkie”: w rzeczywistości ludzie otrzymywali złe wyniki w swoich obliczeniach, zobacz „Jak wartości podrzędne poprawiają obliczenia” poniżej).
Liczby podnormalne
Inżynierowie podrapali się przez chwilę po głowach i jak zwykle wrócili z kolejnym dobrym pomysłem. Co jeśli utworzymy nową regułę:
Jeśli wykładnik wynosi 0, to:
- wiodący bit staje się 0
- wykładnik jest ustalony na -126 (nie -127, jakbyśmy nie mieli tego wyjątku)
Takie liczby nazywane są liczbami podnormalnymi (lub liczbami denormalnymi, które są synonimami).
Ta reguła natychmiast oznacza, że liczba taka, że:
jest nadal 0.0
, co jest dość eleganckie, ponieważ oznacza jedną regułę mniej do śledzenia.
Tak więc 0.0
jest to liczba podnormalna zgodnie z naszą definicją!
Dzięki tej nowej regule najmniejsza liczba nienormalna to:
- wykładnik: 1 (0 byłoby podnormalne)
- frakcja: 0
które reprezentuje:
1.0 * 2 ^ (-126)
Wtedy największa liczba anormalna to:
- wykładnik: 0
- ułamek: 0x7FFFFF (23 bity 1)
co jest równe:
0.FFFFFE * 2 ^ (-126)
gdzie .FFFFFE
jest ponownie 23 bity, jeden na prawo od kropki.
Jest to bardzo blisko najmniejszej wartości nienormalnej, co brzmi rozsądnie.
Najmniejsza niezerowa liczba podnormalna to:
co jest równe:
0.000002 * 2 ^ (-126)
który również wygląda bardzo blisko 0.0
!
Nie mogąc znaleźć żadnego rozsądnego sposobu na przedstawienie liczb mniejszych od tego, inżynierowie byli szczęśliwi i wrócili do oglądania zdjęć kotów w Internecie lub czegokolwiek innego, co robili w latach 70.
Jak widać, liczby podnormalne stanowią kompromis między dokładnością a długością reprezentacji.
Jako najbardziej ekstremalny przykład, najmniejsza niezerowa wartość podnormalna:
0.000002 * 2 ^ (-126)
ma zasadniczo precyzję pojedynczego bitu zamiast 32 bitów. Na przykład, jeśli podzielimy to przez dwa:
0.000002 * 2 ^ (-126) / 2
faktycznie osiągamy 0.0
dokładnie!
Wyobrażanie sobie
Zawsze dobrze jest mieć geometryczną intuicję dotyczącą tego, czego się uczymy, więc proszę bardzo.
Jeśli narysujemy liczby zmiennoprzecinkowe IEEE 754 w linii dla każdego podanego wykładnika, wygląda to mniej więcej tak:
+---+-------+---------------+-------------------------------+
exponent |126| 127 | 128 | 129 |
+---+-------+---------------+-------------------------------+
| | | | |
v v v v v
-------------------------------------------------------------
floats ***** * * * * * * * * * * * *
-------------------------------------------------------------
^ ^ ^ ^ ^
| | | | |
0.5 1.0 2.0 4.0 8.0
Z tego widać, że:
- dla każdego wykładnika nie ma nakładania się między przedstawionymi liczbami
- dla każdego wykładnika mamy tę samą liczbę 2 ^ 32 liczb (tutaj reprezentowaną przez 4
*
)
- w każdym wykładniku punkty są równo rozmieszczone
- większe wykładniki obejmują większe zakresy, ale punkty są bardziej rozłożone
Teraz sprowadzimy to do wykładnika 0.
Bez wartości podnormalnych hipotetycznie wyglądałoby to tak:
+---+---+-------+---------------+-------------------------------+
exponent | ? | 0 | 1 | 2 | 3 |
+---+---+-------+---------------+-------------------------------+
| | | | | |
v v v v v v
-----------------------------------------------------------------
floats * **** * * * * * * * * * * * *
-----------------------------------------------------------------
^ ^ ^ ^ ^ ^
| | | | | |
0 | 2^-126 2^-125 2^-124 2^-123
|
2^-127
W przypadku podnormalnych wygląda to tak:
+-------+-------+---------------+-------------------------------+
exponent | 0 | 1 | 2 | 3 |
+-------+-------+---------------+-------------------------------+
| | | | |
v v v v v
-----------------------------------------------------------------
floats * * * * * * * * * * * * * * * * *
-----------------------------------------------------------------
^ ^ ^ ^ ^ ^
| | | | | |
0 | 2^-126 2^-125 2^-124 2^-123
|
2^-127
Porównując te dwa wykresy, widzimy, że:
podnormalne podwajają długość zakresu wykładnika 0
, od [2^-127, 2^-126)
do[0, 2^-126)
Przestrzeń między elementami zmiennoprzecinkowymi w zakresie poniżej normalnego jest taka sama jak dla [0, 2^-126)
.
zakres [2^-127, 2^-126)
ma połowę liczby punktów, które miałby bez wartości podrzędnych.
Połowa z tych punktów jest przeznaczona na drugą połowę zakresu.
zakres [0, 2^-127)
ma kilka punktów z wartościami podrzędnymi, ale żadnych bez.
Ten brak punktów [0, 2^-127)
nie jest zbyt elegancki i jest głównym powodem istnienia subnormalności!
ponieważ punkty są równomiernie rozmieszczone:
- zakres
[2^-128, 2^-127)
ma połowę punktów niż [2^-127, 2^-126)
- [2^-129, 2^-128)
ma połowę punktów niż[2^-128, 2^-127)
- i tak dalej
To właśnie mamy na myśli, mówiąc, że wartości podnormalne są kompromisem między rozmiarem a precyzją.
Przykład Runnable C.
Zagrajmy teraz z prawdziwym kodem, aby zweryfikować naszą teorię.
W prawie wszystkich obecnych i stacjonarnych komputerach, C float
reprezentuje liczby zmiennoprzecinkowe pojedynczej precyzji IEEE 754.
Dotyczy to w szczególności mojego laptopa Ubuntu 18.04 amd64 Lenovo P51.
Przy takim założeniu wszystkie stwierdzenia przekazują następujący program:
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 upstream .
Skompiluj i uruchom z:
gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c
./subnormal.out
C ++
Oprócz ujawniania wszystkich interfejsów API języka C, C ++ udostępnia również dodatkowe funkcje związane z podnormalizacją, które nie są tak łatwo dostępne w C w <limits>
, np .:
denorm_min
: Zwraca minimalną dodatnią wartość podnormalną typu T
W C ++ całe API jest oparte na szablonach dla każdego typu zmiennoprzecinkowego i jest znacznie ładniejsze.
Wdrożenia
x86_64 i ARMv8 implementują IEEE 754 bezpośrednio na sprzęcie, na który tłumaczy się kod C.
W niektórych implementacjach elementy podnormalne wydają się działać wolniej niż normalne: Dlaczego zmiana z 0,1f na 0 spowalnia wydajność 10-krotnie? Jest to wspomniane w podręczniku ARM, zobacz sekcję „Szczegóły ARMv8” w tej odpowiedzi.
Szczegóły ARMv8
Podręcznik architektury ARM ARMv8 DDI 0487C.a instrukcja A1.5.4 „Zrównanie do zera” opisuje konfigurowalny tryb, w którym wartości podrzędne są zaokrąglane do zera w celu poprawy wydajności:
Wydajność przetwarzania zmiennoprzecinkowego można zmniejszyć podczas wykonywania obliczeń obejmujących zdenormalizowane liczby i wyjątki niedomiaru. W wielu algorytmach wydajność tę można odzyskać bez znaczącego wpływu na dokładność wyniku końcowego, zastępując zdenormalizowane operandy i wyniki pośrednie zerami. Aby umożliwić tę optymalizację, implementacje zmiennoprzecinkowe ARM pozwalają na użycie trybu zerowego dla różnych formatów zmiennoprzecinkowych w następujący sposób:
Dla AArch64:
Jeśli FPCR.FZ==1
, to tryb Flush-to-Zero jest używany dla wszystkich wejść i wyjść Single-Precision i Double-Precision dla wszystkich instrukcji.
Jeśli FPCR.FZ16==1
, to tryb równorzędny do zera jest używany do wszystkich wejść i wyjść instrukcji zmiennoprzecinkowych z połowiczną precyzją, innych niż: —Konwersje między liczbami o połowie dokładności i pojedynczej precyzji. —Konwersje między dokładnością połowiczną a podwójną dokładnością liczby.
A1.5.2 „Standardy zmiennoprzecinkowe i terminologia” Tabela A1-3 „Terminologia zmiennoprzecinkowa” potwierdza, że wartości podnormalne i denormalne są synonimami:
This manual IEEE 754-2008
------------------------- -------------
[...]
Denormal, or denormalized Subnormal
C5.2.7 „FPCR, zmiennoprzecinkowy rejestr sterujący” opisuje, w jaki sposób ARMv8 może opcjonalnie zgłaszać wyjątki lub ustawiać bity flagi, gdy dane wejściowe operacji zmiennoprzecinkowej są nieprawidłowe:
FPCR.IDE, bit [15] Włącz pułapkę wyjątków zmiennoprzecinkowych wejściowych Denormal. Możliwe wartości to:
0b0 Wybrano nieopakowaną obsługę wyjątków. Jeśli wystąpi wyjątek zmiennoprzecinkowy, bit FPSR.IDC jest ustawiany na 1.
0b1 Wybrano obsługę uwięzionych wyjątków. Jeśli wystąpi wyjątek zmiennoprzecinkowy, PE nie aktualizuje bitu FPSR.IDC. Oprogramowanie do obsługi pułapek może zdecydować, czy ustawić bit FPSR.IDC na 1.
D12.2.88 "MVFR1_EL1, AArch32 Media and VFP Feature Register 1" pokazuje, że obsługa denormalna jest w rzeczywistości całkowicie opcjonalna i oferuje trochę do wykrycia, czy jest obsługiwana:
FPFtZ, bity [3: 0]
Tryb spłukiwania do zera. Wskazuje, czy implementacja zmiennoprzecinkowa zapewnia obsługę tylko dla trybu działania Flush-to-Zero. Zdefiniowane wartości to:
Wszystkie inne wartości są zastrzeżone.
W ARMv8-A dozwolone wartości to 0b0000 i 0b0001.
Sugeruje to, że jeśli podnormalne nie są zaimplementowane, implementacje po prostu powracają do zerowania.
Nieskończoność i NaN
Ciekawy? Napisałem kilka rzeczy na:
Jak wartości podnormalne poprawiają obliczenia
DO ZROBIENIA: dokładniejsze zrozumienie, w jaki sposób ten skok pogarsza wyniki obliczeń / jak wartości podnormalne poprawiają wyniki obliczeń.
Aktualna historia
The Interview with the Old Man of Floating-Point autorstwa Charlesa Severance'a (1998) to krótki przegląd historii świata w formie wywiadu z Williamem Kahanem, zasugerowany przez Johna Colemana w komentarzach.