Wie sehen qNaNs und sNaNs experimentell aus?
Lassen Sie uns zunächst lernen, wie wir feststellen können, ob wir einen sNaN oder einen qNaN haben.
Ich werde in dieser Antwort C ++ anstelle von C verwenden, da es das Praktische bietet std::numeric_limits::quiet_NaN
und std::numeric_limits::signaling_NaN
das ich in C nicht bequem finden konnte.
Ich konnte jedoch keine Funktion zum Klassifizieren finden, wenn ein NaN sNaN oder qNaN ist. Drucken wir also einfach die NaN-Rohbytes aus:
main.cpp
#include <cassert>
#include <cstring>
#include <cmath> // nanf, isnan
#include <iostream>
#include <limits> // std::numeric_limits
#pragma STDC FENV_ACCESS ON
void print_float(float f) {
std::uint32_t i;
std::memcpy(&i, &f, sizeof f);
std::cout << std::hex << i << std::endl;
}
int main() {
static_assert(std::numeric_limits<float>::has_quiet_NaN, "");
static_assert(std::numeric_limits<float>::has_signaling_NaN, "");
static_assert(std::numeric_limits<float>::has_infinity, "");
// Generate them.
float qnan = std::numeric_limits<float>::quiet_NaN();
float snan = std::numeric_limits<float>::signaling_NaN();
float inf = std::numeric_limits<float>::infinity();
float nan0 = std::nanf("0");
float nan1 = std::nanf("1");
float nan2 = std::nanf("2");
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
// Print their bytes.
std::cout << "qnan "; print_float(qnan);
std::cout << "snan "; print_float(snan);
std::cout << " inf "; print_float(inf);
std::cout << "-inf "; print_float(-inf);
std::cout << "nan0 "; print_float(nan0);
std::cout << "nan1 "; print_float(nan1);
std::cout << "nan2 "; print_float(nan2);
std::cout << " 0/0 "; print_float(div_0_0);
std::cout << "sqrt "; print_float(sqrt_negative);
// Assert if they are NaN or not.
assert(std::isnan(qnan));
assert(std::isnan(snan));
assert(!std::isnan(inf));
assert(!std::isnan(-inf));
assert(std::isnan(nan0));
assert(std::isnan(nan1));
assert(std::isnan(nan2));
assert(std::isnan(div_0_0));
assert(std::isnan(sqrt_negative));
}
Kompilieren und ausführen:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
Ausgabe auf meinem x86_64-Computer:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Wir können das Programm auch auf aarch64 im QEMU-Benutzermodus ausführen:
aarch64-linux-gnu-g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
qemu-aarch64 -L /usr/aarch64-linux-gnu/ main.out
und das erzeugt genau die gleiche Ausgabe, was darauf hindeutet, dass mehrere Bögen IEEE 754 eng implementieren.
Wenn Sie an dieser Stelle nicht mit der Struktur von IEEE 754-Gleitkommazahlen vertraut sind, schauen Sie sich Folgendes an: Was ist eine subnormale Gleitkommazahl?
In binärer Form sind einige der obigen Werte:
31
|
| 30 23 22 0
| | | | |
-----+-+------+-+---------------------+
qnan 0 11111111 10000000000000000000000
snan 0 11111111 01000000000000000000000
inf 0 11111111 00000000000000000000000
-inf 1 11111111 00000000000000000000000
-----+-+------+-+---------------------+
| | | | |
| +------+ +---------------------+
| | |
| v v
| exponent fraction
|
v
sign
Aus diesem Experiment beobachten wir, dass:
qNaN und sNaN scheinen nur durch Bit 22 unterschieden zu werden: 1 bedeutet leise und 0 bedeutet Signalisierung
Unendlichkeiten sind auch mit Exponent == 0xFF ziemlich ähnlich, aber sie haben Bruch == 0.
Aus diesem Grund müssen NaNs Bit 21 auf 1 setzen, sonst wäre es nicht möglich, sNaN von positiver Unendlichkeit zu unterscheiden!
nanf()
erzeugt mehrere verschiedene NaNs, daher muss es mehrere mögliche Codierungen geben:
7fc00000
7fc00001
7fc00002
Da dies nan0
dasselbe ist wie std::numeric_limits<float>::quiet_NaN()
, schließen wir, dass es sich bei allen um unterschiedliche leise NaNs handelt.
Der Standardentwurf C11 N1570 bestätigt, dass nanf()
leise NaNs erzeugt werden, da Weiterleitungen nanf
an strtod
und 7.22.1.3 "Die Funktionen strtod, strtof und strtold" lauten:
Eine Zeichenfolge NAN oder NAN (n-char-sequence opt) wird als leises NaN interpretiert, wenn sie im Rückgabetyp unterstützt wird, ansonsten wie ein Subjektsequenzteil, das nicht die erwartete Form hat. Die Bedeutung der n-Zeichen-Sequenz ist implementierungsdefiniert. 293)
Siehe auch:
Wie sehen qNaNs und sNaNs in den Handbüchern aus?
IEEE 754 2008 empfiehlt Folgendes (TODO obligatorisch oder optional?):
- Alles mit Exponent == 0xFF und Bruch! = 0 ist ein NaN
- und dass das höchste Bruchbit qNaN von sNaN unterscheidet
aber es scheint nicht zu sagen, welches Bit bevorzugt wird, um die Unendlichkeit von NaN zu unterscheiden.
6.2.1 "NaN-Codierungen in binären Formaten" sagt:
Dieser Unterabschnitt spezifiziert ferner die Codierungen von NaNs als Bitfolgen, wenn sie das Ergebnis von Operationen sind. Bei der Codierung haben alle NaNs ein Vorzeichenbit und ein Muster von Bits, die erforderlich sind, um die Codierung als NaN zu identifizieren, und die ihre Art bestimmt (sNaN vs. qNaN). Die verbleibenden Bits, die sich im nachfolgenden Signifikantenfeld befinden, codieren die Nutzlast, bei der es sich möglicherweise um Diagnoseinformationen handelt (siehe oben). 34
Bei allen binären NaN-Bitfolgen sind alle Bits des vorgespannten Exponentenfelds E auf 1 gesetzt (siehe 3.4). Eine ruhige NaN-Bitfolge sollte codiert werden, wobei das erste Bit (d1) des nachlaufenden Signifikantenfelds T 1 ist. Eine signalisierende NaN-Bitfolge sollte codiert werden, wobei das erste Bit des nachfolgenden Signifikantenfelds 0 ist. Wenn das erste Bit der Das nachfolgende Signifikantenfeld ist 0, ein anderes Bit des nachfolgenden Signifikantenfelds muss ungleich Null sein, um das NaN von der Unendlichkeit zu unterscheiden. In der gerade beschriebenen bevorzugten Codierung soll ein Signalisierungs-NaN durch Setzen von d1 auf 1 beruhigt werden, wobei die verbleibenden Bits von T unverändert bleiben. Bei binären Formaten wird die Nutzlast in den niedrigstwertigen p-2-Bits des nachfolgenden Signifikantenfelds codiert
Das Softwareentwicklerhandbuch für Intel 64- und IA-32-Architekturen - Band 1 - Grundlegende Architektur - 253665-056US September 2015 4.8.3.4 "NaNs" bestätigen, dass x86 IEEE 754 folgt, indem NaN und sNaN durch das Bit mit dem höchsten Bruch unterschieden werden:
Die IA-32-Architektur definiert zwei Klassen von NaNs: leise NaNs (QNaNs) und signalisierende NaNs (SNaNs). Ein QNaN ist ein NaN, bei dem das höchstwertige Bruchteil gesetzt ist. Ein SNaN ist ein NaN, bei dem das höchstwertige Bruchteil frei ist.
und das ARM Architecture-Referenzhandbuch - ARMv8 für das ARMv8-A-Architekturprofil - DDI 0487C.a A1.4.3 "Gleitkommaformat mit einfacher Genauigkeit":
fraction != 0
: Der Wert ist ein NaN und entweder ein leises NaN oder ein signalisierendes NaN. Die beiden Arten von NaN unterscheiden sich durch ihr höchstwertiges Bruchbit, Bit [22]:
bit[22] == 0
: Das NaN ist ein signalisierendes NaN. Das Vorzeichenbit kann einen beliebigen Wert annehmen, und die verbleibenden Bruchbits können einen beliebigen Wert außer allen Nullen annehmen.
bit[22] == 1
: Das NaN ist ein leises NaN. Das Vorzeichenbit und die verbleibenden Bruchbits können einen beliebigen Wert annehmen.
Wie werden qNanS und sNaNs generiert?
Ein Hauptunterschied zwischen qNaNs und sNaNs besteht darin, dass:
- qNaN wird durch reguläre integrierte (Software oder Hardware) arithmetische Operationen mit seltsamen Werten generiert
- sNaN wird niemals durch eingebaute Operationen erzeugt, sondern kann nur von Programmierern explizit hinzugefügt werden, z. B. mit
std::numeric_limits::signaling_NaN
Ich konnte keine eindeutigen IEEE 754- oder C11-Anführungszeichen dafür finden, aber ich kann auch keine eingebaute Operation finden, die sNaNs erzeugt ;-)
Das Intel-Handbuch gibt dieses Prinzip jedoch unter 4.8.3.4 "NaNs" deutlich an:
SNaNs werden normalerweise verwendet, um einen Ausnahmebehandler abzufangen oder aufzurufen. Sie müssen per Software eingefügt werden. Das heißt, der Prozessor generiert niemals eine SNaN als Ergebnis einer Gleitkommaoperation.
Dies ist aus unserem Beispiel ersichtlich, in dem beide:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
produzieren genau die gleichen Bits wie std::numeric_limits<float>::quiet_NaN()
.
Beide Vorgänge werden zu einer einzigen x86-Assemblyanweisung kompiliert, die den qNaN direkt in der Hardware generiert (TODO-Bestätigung mit GDB).
Was machen qNaNs und sNaNs unterschiedlich?
Jetzt, da wir wissen, wie qNaNs und sNaNs aussehen und wie man sie manipuliert, sind wir endlich bereit, sNaNs dazu zu bringen, ihr Ding zu machen und einige Programme in die Luft zu jagen!
Also ohne weiteres:
blow_up.cpp
#include <cassert>
#include <cfenv>
#include <cmath> // isnan
#include <iostream>
#include <limits> // std::numeric_limits
#include <unistd.h>
#pragma STDC FENV_ACCESS ON
int main() {
float snan = std::numeric_limits<float>::signaling_NaN();
float qnan = std::numeric_limits<float>::quiet_NaN();
float f;
// No exceptions.
assert(std::fetestexcept(FE_ALL_EXCEPT) == 0);
// Still no exceptions because qNaN.
f = qnan + 1.0f;
assert(std::isnan(f));
if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
std::cout << "FE_ALL_EXCEPT qnan + 1.0f" << std::endl;
// Now we can get an exception because sNaN, but signals are disabled.
f = snan + 1.0f;
assert(std::isnan(f));
if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
std::cout << "FE_ALL_EXCEPT snan + 1.0f" << std::endl;
feclearexcept(FE_ALL_EXCEPT);
// And now we enable signals and blow up with SIGFPE! >:-)
feenableexcept(FE_INVALID);
f = qnan + 1.0f;
std::cout << "feenableexcept qnan + 1.0f" << std::endl;
f = snan + 1.0f;
std::cout << "feenableexcept snan + 1.0f" << std::endl;
}
Kompilieren, ausführen und den Exit-Status erhalten:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Ausgabe:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Beachten Sie, dass dieses Verhalten nur -O0
in GCC 8.2 auftritt: mit-O3
auftritt berechnet GCC alle unsere sNaN-Operationen vorab und optimiert sie! Ich bin mir nicht sicher, ob es eine standardkonforme Möglichkeit gibt, dies zu verhindern.
Aus diesem Beispiel schließen wir also:
snan + 1.0
Ursachen FE_INVALID
, aber qnan + 1.0
nicht
Linux erzeugt nur dann ein Signal, wenn es mit aktiviert ist feenableexept
.
Dies ist eine glibc-Erweiterung, ich konnte in keinem Standard einen Weg finden, dies zu tun.
Wenn das Signal auftritt, wird die CPU-Hardware selbst eine Ausnahme auslösen, die der Linux-Kernel behandelt und die Anwendung über das Signal informiert hat.
Das Ergebnis ist , dass bash druckt Floating point exception (core dumped)
, und der Ausgangszustand ist 136
, der dem entspricht , Signal 136 - 128 == 8
, das gemäß:
man 7 signal
ist SIGFPE
.
Beachten Sie, dass dies SIGFPE
dasselbe Signal ist, das wir erhalten, wenn wir versuchen, eine Ganzzahl durch 0 zu teilen:
int main() {
int i = 1 / 0;
}
obwohl für ganze Zahlen:
- Wenn Sie etwas durch Null teilen, wird das Signal erhöht, da es keine Ganzzahldarstellung in ganzen Zahlen gibt
- das Signal, das standardmäßig auftritt, ohne dass dies erforderlich ist
feenableexcept
Wie gehe ich mit dem SIGFPE um?
Wenn Sie nur einen Handler erstellen, der normal zurückgibt, führt dies zu einer Endlosschleife, da die Division nach der Rückkehr des Handlers erneut erfolgt! Dies kann mit GDB überprüft werden.
Die einzige Möglichkeit besteht darin, wie folgt zu verwenden setjmp
und longjmp
an einen anderen Ort zu springen: C-Handle-Signal SIGFPE und Fortsetzung der Ausführung
Was sind einige reale Anwendungen von sNaNs?
Ganz ehrlich, ich habe immer noch keinen besonders nützlichen Anwendungsfall für sNaNs verstanden. Dies wurde gefragt unter: Nützlichkeit der Signalisierung von NaN?
sNaNs fühlen sich besonders nutzlos an, weil wir die anfänglich ungültigen Operationen ( 0.0f/0.0f
) erkennen können, die qNaNs erzeugen mit feenableexcept
: Es scheint, dass snan
nur Fehler für weitere Operationen ausgelöst werden, die nicht ausgelöst werden qnan
, z.qnan + 1.0f
. ).
Z.B:
Haupt c
#define _GNU_SOURCE
#include <fenv.h>
#include <stdio.h>
int main(int argc, char **argv) {
(void)argv;
float f0 = 0.0;
if (argc == 1) {
feenableexcept(FE_INVALID);
}
float f1 = 0.0 / f0;
printf("f1 %f\n", f1);
feenableexcept(FE_INVALID);
float f2 = f1 + 1.0;
printf("f2 %f\n", f2);
}
kompilieren:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
dann:
./main.out
gibt:
Floating point exception (core dumped)
und:
./main.out 1
gibt:
f1 -nan
f2 -nan
Siehe auch: So verfolgen Sie ein NaN in C ++
Was sind die Signalflags und wie werden sie manipuliert?
Alles ist in der CPU-Hardware implementiert.
Die Flags befinden sich in einem Register, ebenso wie das Bit, das angibt, ob eine Ausnahme / ein Signal ausgelöst werden soll.
Diese Register sind vom Benutzerland aus zugänglich von den meisten Bögen aus aus .
Dieser Teil des glibc 2.29-Codes ist eigentlich sehr einfach zu verstehen!
fetestexcept
Wird beispielsweise für x86_86 unter sysdeps / x86_64 / fpu / ftestexcept.c implementiert :
#include <fenv.h>
int
fetestexcept (int excepts)
{
int temp;
unsigned int mxscr;
/* Get current exceptions. */
__asm__ ("fnstsw %0\n"
"stmxcsr %1" : "=m" (*&temp), "=m" (*&mxscr));
return (temp | mxscr) & excepts & FE_ALL_EXCEPT;
}
libm_hidden_def (fetestexcept)
so sehen wir sofort, dass die anweisungen verwendet werden stmxcsr
für "Store MXCSR Register State" steht.
Und feenableexcept
ist implementiert unter sysdeps / x86_64 / fpu / feenablxcpt.c :
#include <fenv.h>
int
feenableexcept (int excepts)
{
unsigned short int new_exc, old_exc;
unsigned int new;
excepts &= FE_ALL_EXCEPT;
/* Get the current control word of the x87 FPU. */
__asm__ ("fstcw %0" : "=m" (*&new_exc));
old_exc = (~new_exc) & FE_ALL_EXCEPT;
new_exc &= ~excepts;
__asm__ ("fldcw %0" : : "m" (*&new_exc));
/* And now the same for the SSE MXCSR register. */
__asm__ ("stmxcsr %0" : "=m" (*&new));
/* The SSE exception masks are shifted by 7 bits. */
new &= ~(excepts << 7);
__asm__ ("ldmxcsr %0" : : "m" (*&new));
return old_exc;
}
Was sagt der C-Standard über qNaN vs sNaN?
Der Standardentwurf C11 N1570 besagt ausdrücklich, dass der Standard bei F.2.1 "Unendlichkeiten, vorzeichenbehaftete Nullen und NaNs" nicht zwischen ihnen unterscheidet:
1 Diese Spezifikation definiert nicht das Verhalten der Signalisierung von NaNs. Es wird allgemein der Begriff NaN verwendet, um ruhige NaNs zu bezeichnen. Die NAN- und INFINITY-Makros sowie die Nan-Funktionen enthalten <math.h>
Bezeichnungen für NaNs und Infinities nach IEC 60559.
Getestet in Ubuntu 18.10, GCC 8.2. GitHub Upstreams: