IEEE 754-Grundlagen
Lassen Sie uns zunächst die Grundlagen der Organisation von IEEE 754-Nummern überprüfen.
Wir werden uns auf die einfache Genauigkeit (32-Bit) konzentrieren, aber alles kann sofort auf andere Genauigkeiten verallgemeinert werden.
Das Format ist:
- 1 Bit: Vorzeichen
- 8 Bits: Exponent
- 23 Bit: Bruch
Oder wenn Sie Bilder mögen:
Quelle .
Das Vorzeichen ist einfach: 0 ist positiv und 1 ist negativ, Ende der Geschichte.
Der Exponent ist 8 Bit lang und reicht daher von 0 bis 255.
Der Exponent wird als vorgespannt bezeichnet, weil er einen Versatz von -127
z.
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
Die führende Bitkonvention
Beim Entwurf von IEEE 754 stellten die Ingenieure fest, dass alle Zahlen außer 0.0
einer 1
binären Eins als erste Ziffer haben. Z.B:
25.0 == (binary) 11001 == 1.1001 * 2^4
0.625 == (binary) 0.101 == 1.01 * 2^-1
beide beginnen mit diesem nervigen 1.
Teil.
Daher wäre es verschwenderisch, diese Ziffer fast jede einzelne Zahl mit einem Präzisionsbit belegen zu lassen.
Aus diesem Grund haben sie die "Leitbitkonvention" erstellt:
Nehmen Sie immer an, dass die Nummer mit eins beginnt
Aber wie geht man dann damit um 0.0
? Nun, sie haben beschlossen, eine Ausnahme zu erstellen:
- wenn der Exponent 0 ist
- und der Bruch ist 0
- dann steht die Zahl für Plus oder Minus
0.0
damit stellen die bytes 00 00 00 00
auch dar 0.0
, was gut aussieht.
Wenn wir nur diese Regeln berücksichtigen würden, wäre die kleinste Zahl ungleich Null, die dargestellt werden kann:
was aufgrund der führenden Bitkonvention in einem Hex-Bruch ungefähr so aussieht:
1.000002 * 2 ^ (-127)
wo .000002
ist 22 Nullen mit einem 1
am Ende.
Wir können nicht nehmen fraction = 0
, sonst wäre diese Nummer 0.0
.
Aber dann dachten die Ingenieure, die auch einen ausgeprägten ästhetischen Sinn hatten: Ist das nicht hässlich? Dass wir von direkt 0.0
zu etwas springen , das nicht einmal eine richtige Potenz von 2 ist? Könnten wir nicht irgendwie noch kleinere Zahlen darstellen?
Subnormale Zahlen
Die Ingenieure kratzten sich eine Weile am Kopf und kamen wie üblich mit einer weiteren guten Idee zurück. Was ist, wenn wir eine neue Regel erstellen:
Wenn der Exponent 0 ist, dann:
- Das führende Bit wird 0
- Der Exponent ist auf -126 festgelegt (nicht -127, als ob wir diese Ausnahme nicht hätten).
Solche Zahlen werden als subnormale Zahlen (oder Denormalzahlen, was synonym ist) bezeichnet.
Diese Regel impliziert sofort, dass die Nummer so ist, dass:
ist immer noch 0.0
, was irgendwie elegant ist, da es eine Regel weniger bedeutet, den Überblick zu behalten.
Also 0.0
ist eigentlich eine subnormale Zahl nach unserer Definition!
Mit dieser neuen Regel lautet die kleinste nicht subnormale Zahl:
- Exponent: 1 (0 wäre subnormal)
- Bruchteil: 0
welches darstellt:
1.0 * 2 ^ (-126)
Dann ist die größte subnormale Zahl:
- Exponent: 0
- Bruch: 0x7FFFFF (23 Bit 1)
was gleich ist:
0.FFFFFE * 2 ^ (-126)
wo .FFFFFE
ist wieder 23 Bits eins rechts vom Punkt.
Dies ist ziemlich nahe an der kleinsten nicht-subnormalen Zahl, was vernünftig klingt.
Und die kleinste subnormale Zahl ungleich Null ist:
was gleich ist:
0.000002 * 2 ^ (-126)
das sieht auch ziemlich nah aus 0.0
!
Die Ingenieure waren nicht in der Lage, einen vernünftigen Weg zu finden, um kleinere Zahlen darzustellen. Sie waren glücklich und schauten sich wieder Katzenbilder online an oder was auch immer sie in den 70er Jahren taten.
Wie Sie sehen können, machen subnormale Zahlen einen Kompromiss zwischen Genauigkeit und Darstellungslänge.
Als extremstes Beispiel das kleinste Subnormal ungleich Null:
0.000002 * 2 ^ (-126)
hat im Wesentlichen eine Genauigkeit von einem einzelnen Bit anstelle von 32 Bit. Wenn wir es zum Beispiel durch zwei teilen:
0.000002 * 2 ^ (-126) / 2
wir erreichen tatsächlich 0.0
genau!
Visualisierung
Es ist immer eine gute Idee, eine geometrische Intuition über das zu haben, was wir lernen.
Wenn wir für jeden Exponenten IEEE 754-Gleitkommazahlen auf einer Linie darstellen, sieht dies ungefähr so aus:
+---+-------+---------------+-------------------------------+
exponent |126| 127 | 128 | 129 |
+---+-------+---------------+-------------------------------+
| | | | |
v v v v v
-------------------------------------------------------------
floats ***** * * * * * * * * * * * *
-------------------------------------------------------------
^ ^ ^ ^ ^
| | | | |
0.5 1.0 2.0 4.0 8.0
Daraus können wir das ersehen:
- Für jeden Exponenten gibt es keine Überlappung zwischen den dargestellten Zahlen
- Für jeden Exponenten haben wir die gleiche Zahl 2 ^ 32 von Zahlen (hier dargestellt durch 4
*
).
- Innerhalb jedes Exponenten sind die Punkte gleich beabstandet
- Größere Exponenten decken größere Bereiche ab, wobei die Punkte jedoch weiter verteilt sind
Lassen Sie uns das jetzt bis zum Exponenten 0 reduzieren.
Ohne Subnormen würde es hypothetisch so aussehen:
+---+---+-------+---------------+-------------------------------+
exponent | ? | 0 | 1 | 2 | 3 |
+---+---+-------+---------------+-------------------------------+
| | | | | |
v v v v v v
-----------------------------------------------------------------
floats * **** * * * * * * * * * * * *
-----------------------------------------------------------------
^ ^ ^ ^ ^ ^
| | | | | |
0 | 2^-126 2^-125 2^-124 2^-123
|
2^-127
Bei Subnormen sieht es so aus:
+-------+-------+---------------+-------------------------------+
exponent | 0 | 1 | 2 | 3 |
+-------+-------+---------------+-------------------------------+
| | | | |
v v v v v
-----------------------------------------------------------------
floats * * * * * * * * * * * * * * * * *
-----------------------------------------------------------------
^ ^ ^ ^ ^ ^
| | | | | |
0 | 2^-126 2^-125 2^-124 2^-123
|
2^-127
Beim Vergleich der beiden Diagramme sehen wir Folgendes:
Subnormen verdoppeln die Länge des Exponentenbereichs 0
von [2^-127, 2^-126)
bis[0, 2^-126)
Der Abstand zwischen den Floats im subnormalen Bereich ist der gleiche wie für [0, 2^-126)
.
Der Bereich [2^-127, 2^-126)
hat die Hälfte der Punkte, die er ohne Subnormen hätte.
Die Hälfte dieser Punkte füllt die andere Hälfte des Bereichs.
Der Bereich [0, 2^-127)
hat einige Punkte mit Subnormen, aber keine ohne.
Dieser Mangel an Punkten [0, 2^-127)
ist nicht sehr elegant und der Hauptgrund für die Existenz von Subnormen!
da die Punkte gleich beabstandet sind:
- Der Bereich
[2^-128, 2^-127)
hat die Hälfte der Punkte als [2^-127, 2^-126)
- [2^-129, 2^-128)
hat die Hälfte der Punkte als[2^-128, 2^-127)
- und so weiter
Dies ist es, was wir meinen, wenn wir sagen, dass Subnormen ein Kompromiss zwischen Größe und Präzision sind.
Runnable C Beispiel
Lassen Sie uns nun mit einem tatsächlichen Code spielen, um unsere Theorie zu verifizieren.
In fast allen aktuellen und Desktop-Computern steht C float
für IEEE 754-Gleitkommazahlen mit einfacher Genauigkeit.
Dies gilt insbesondere für meinen Ubuntu 18.04 amd64 Lenovo P51 Laptop.
Mit dieser Annahme geben alle Behauptungen das folgende Programm weiter:
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 stromaufwärts .
Kompilieren und ausführen mit:
gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c
./subnormal.out
C ++
C ++ stellt nicht nur alle APIs von C zur Verfügung, sondern stellt auch einige zusätzliche subnormale Funktionen zur Verfügung, die in C nicht so leicht verfügbar sind <limits>
, z.
denorm_min
: Gibt den minimalen positiven Subnormalwert vom Typ T zurück
In C ++ wird die gesamte API für jeden Gleitkommatyp als Vorlage verwendet und ist viel besser.
Implementierungen
x86_64 und ARMv8 implementieren IEEE 754 direkt auf der Hardware, in die der C-Code übersetzt wird.
Subnormen scheinen in bestimmten Implementierungen weniger schnell als normal zu sein : Warum verlangsamt das Ändern von 0,1f auf 0 die Leistung um das 10-fache? Dies wird im ARM-Handbuch erwähnt, siehe Abschnitt "ARMv8-Details" dieser Antwort.
ARMv8-Details
ARM-Architektur Referenzhandbuch ARMv8 DDI 0487C.a Handbuch A1.5.4 "Auf Null spülen" beschreibt einen konfigurierbaren Modus, in dem Subnormen zur Verbesserung der Leistung auf Null gerundet werden:
Die Leistung der Gleitkommaverarbeitung kann reduziert werden, wenn Berechnungen mit denormalisierten Zahlen und Unterlaufausnahmen durchgeführt werden. In vielen Algorithmen kann diese Leistung wiederhergestellt werden, ohne die Genauigkeit des Endergebnisses wesentlich zu beeinträchtigen, indem die denormalisierten Operanden und Zwischenergebnisse durch Nullen ersetzt werden. Um diese Optimierung zu ermöglichen, ermöglichen ARM-Gleitkommaimplementierungen die Verwendung eines Flush-to-Zero-Modus für verschiedene Gleitkommaformate wie folgt:
Für AArch64:
Wenn dies der FPCR.FZ==1
Fall ist, wird der Flush-to-Zero-Modus für alle Ein- und Ausgänge aller Anweisungen mit einfacher und doppelter Präzision verwendet.
Wenn dies der FPCR.FZ16==1
Fall ist, wird der Flush-to-Zero-Modus für alle Ein- und Ausgänge von Gleitkommaanweisungen mit halber Präzision verwendet, außer: - Konvertierungen zwischen Halbpräzisions- und Einfachpräzisionszahlen. - Konvertierungen zwischen Halbpräzision und Doppelpräzision Zahlen.
A1.5.2 "Gleitkomma-Standards und Terminologie" Tabelle A1-3 "Gleitkomma-Terminologie" bestätigt, dass Subnormen und Denormale Synonyme sind:
This manual IEEE 754-2008
------------------------- -------------
[...]
Denormal, or denormalized Subnormal
C5.2.7 "FPCR, Gleitkomma-Steuerregister" beschreibt, wie ARMv8 optional Ausnahmen auslösen oder Flag-Bits setzen kann, wenn die Eingabe einer Gleitkommaoperation nicht normal ist:
FPCR.IDE, Bit [15] Denormal-Gleitkomma-Ausnahmefalle aktivieren. Mögliche Werte sind:
0b0 Nicht eingeschlossene Ausnahmebehandlung ausgewählt. Wenn die Gleitkomma-Ausnahme auftritt, wird das FPSR.IDC-Bit auf 1 gesetzt.
0b1 Trapped Exception Handling ausgewählt. Wenn die Gleitkomma-Ausnahme auftritt, aktualisiert das PE das FPSR.IDC-Bit nicht. Die Trap-Handling-Software kann entscheiden, ob das FPSR.IDC-Bit auf 1 gesetzt werden soll.
D12.2.88 "MVFR1_EL1-, AArch32-Medien- und VFP-Funktionsregister 1" zeigt, dass die denormale Unterstützung tatsächlich völlig optional ist, und bietet eine Möglichkeit, festzustellen, ob Unterstützung vorhanden ist:
FPFtZ, Bits [3: 0]
In den Nullmodus spülen. Gibt an, ob die Gleitkommaimplementierung nur den Betriebsmodus "Auf Null spülen" unterstützt. Definierte Werte sind:
Alle anderen Werte sind reserviert.
In ARMv8-A sind die zulässigen Werte 0b0000 und 0b0001.
Dies deutet darauf hin, dass Implementierungen, wenn keine Subnormen implementiert sind, einfach auf Null zurückgesetzt werden.
Unendlichkeit und NaN
Neugierig? Ich habe einige Dinge geschrieben unter: