Come appaiono sperimentalmente qNaN e sNaN?
Impariamo prima come identificare se abbiamo un sNaN o un qNaN.
Userò C ++ in questa risposta invece di C perché offre il conveniente std::numeric_limits::quiet_NaN
e std::numeric_limits::signaling_NaN
che non sono riuscito a trovare in C convenientemente.
Tuttavia non sono riuscito a trovare una funzione per classificare se un NaN è sNaN o qNaN, quindi stampiamo semplicemente i byte grezzi NaN:
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));
}
Compila ed esegui:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
output sulla mia macchina x86_64:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Possiamo anche eseguire il programma su aarch64 con la modalità utente QEMU:
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
e questo produce lo stesso identico output, suggerendo che più archi implementano strettamente IEEE 754.
A questo punto, se non hai familiarità con la struttura dei numeri in virgola mobile IEEE 754, dai un'occhiata a: Cos'è un numero in virgola mobile subnormale?
In binario alcuni dei valori sopra sono:
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
Da questo esperimento osserviamo che:
qNaN e sNaN sembrano differenziarsi solo dal bit 22: 1 significa silenzioso e 0 significa segnalazione
Gli infiniti sono anche abbastanza simili con esponente == 0xFF, ma hanno frazione == 0.
Per questo motivo, NaNs deve impostare il bit 21 a 1, altrimenti non sarebbe possibile distinguere sNaN dall'infinito positivo!
nanf()
produce diversi NaN, quindi devono esserci più codifiche possibili:
7fc00000
7fc00001
7fc00002
Poiché nan0
è uguale a std::numeric_limits<float>::quiet_NaN()
, deduciamo che sono tutti NaN silenziosi diversi.
La bozza standard C11 N1570 conferma che nanf()
genera NaN silenziosi, perché nanf
inoltra a strtod
e 7.22.1.3 "Le funzioni strtod, strtof e strtold" dice:
Una sequenza di caratteri NAN o NAN (n-char-sequence opt) viene interpretata come un NaN silenzioso, se supportato nel tipo restituito, altrimenti come una parte della sequenza di soggetti che non ha la forma prevista; il significato della sequenza di n caratteri è definito dall'implementazione. 293)
Guarda anche:
Come appaiono i qNaN e i sNaN nei manuali?
IEEE 754 2008 raccomanda che (TODO obbligatorio o facoltativo?):
- qualsiasi cosa con esponente == 0xFF e frazione! = 0 è un NaN
- e che il bit della frazione più alta differenzia qNaN da sNaN
ma non sembra dire quale bit sia preferito per differenziare l'infinito da NaN.
6.2.1 "Codifiche NaN in formati binari" dice:
Questa clausola secondaria specifica ulteriormente le codifiche di NaN come stringhe di bit quando sono il risultato di operazioni. Quando codificati, tutti i NaN hanno un bit di segno e uno schema di bit necessari per identificare la codifica come NaN e che ne determina il tipo (sNaN vs. qNaN). I bit rimanenti, che si trovano nel campo del significato finale, codificano il carico utile, che potrebbe essere un'informazione diagnostica (vedere sopra). 34
Tutte le stringhe di bit NaN binarie hanno tutti i bit del campo esponente polarizzato E impostati a 1 (vedere 3.4). Una stringa di bit NaN silenziosa deve essere codificata con il primo bit (d1) del campo di significato finale T che è 1. Una stringa di bit NaN di segnalazione dovrebbe essere codificata con il primo bit del campo di significato finale e 0. Se il primo bit del Il campo significante finale è 0, qualche altro bit del campo significante finale deve essere diverso da zero per distinguere il NaN dall'infinito. Nella codifica preferita appena descritta, una segnalazione NaN deve essere silenziata impostando d1 a 1, lasciando invariati i bit rimanenti di T. Per i formati binari, il carico utile è codificato nei p − 2 bit meno significativi del campo significante finale
Il Manuale dello sviluppatore del software per le architetture Intel 64 e IA-32 - Volume 1 Architettura di base - 253665-056US Settembre 2015 4.8.3.4 "NaNs" conferma che x86 segue IEEE 754 distinguendo NaN e sNaN per la frazione di bit più alta:
L'architettura IA-32 definisce due classi di NaN: NaN silenziosi (QNaN) e NaN di segnalazione (SNaN). Un QNaN è un NaN con la frazione più significativa impostata e SNaN è un NaN con la frazione più significativa chiara.
così come il manuale di riferimento dell'architettura ARM - ARMv8, per il profilo dell'architettura ARMv8-A - DDI 0487C.a A1.4.3 "Formato a virgola mobile a precisione singola":
fraction != 0
: Il valore è un NaN ed è un NaN silenzioso o un NaN di segnalazione. I due tipi di NaN si distinguono per il loro bit di frazione più significativo, bit [22]:
bit[22] == 0
: Il NaN è un NaN di segnalazione. Il bit di segno può assumere qualsiasi valore e i bit di frazione rimanenti possono assumere qualsiasi valore tranne tutti gli zeri.
bit[22] == 1
: Il NaN è un NaN silenzioso. Il bit di segno e i bit di frazione rimanenti possono assumere qualsiasi valore.
Come vengono generati qNanS e sNaNs?
Una delle principali differenze tra qNaN e sNaN è che:
- qNaN è generato da normali operazioni aritmetiche integrate (software o hardware) con valori strani
- sNaN non viene mai generato da operazioni integrate, può essere aggiunto solo esplicitamente dai programmatori, ad esempio con
std::numeric_limits::signaling_NaN
Non sono riuscito a trovare citazioni IEEE 754 o C11 chiare per questo, ma non riesco nemmeno a trovare alcuna operazione incorporata che generi sNaN ;-)
Il manuale Intel afferma chiaramente questo principio tuttavia in 4.8.3.4 "NaNs":
Gli SNaN vengono in genere utilizzati per intercettare o richiamare un gestore di eccezioni. Devono essere inseriti da software; ovvero, il processore non genera mai un SNaN come risultato di un'operazione in virgola mobile.
Questo può essere visto dal nostro esempio in cui entrambi:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
producono esattamente gli stessi bit di std::numeric_limits<float>::quiet_NaN()
.
Entrambe queste operazioni vengono compilate in un'unica istruzione di assembly x86 che genera il qNaN direttamente nell'hardware (TODO confermare con GDB).
Cosa fanno in modo diverso qNaN e sNaN?
Ora che sappiamo che aspetto hanno qNaN e sNaN e come manipolarli, siamo finalmente pronti per provare a fare in modo che i sNaN facciano le loro cose e far saltare in aria alcuni programmi!
Quindi, senza ulteriori indugi:
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;
}
Compila, esegui e ottieni lo stato di uscita:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Produzione:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Nota che questo comportamento si verifica solo -O0
in GCC 8.2: con -O3
, GCC precalcola e ottimizza tutte le nostre operazioni sNaN! Non sono sicuro che esista un modo conforme agli standard per impedirlo.
Quindi deduciamo da questo esempio che:
snan + 1.0
cause FE_INVALID
, ma qnan + 1.0
non lo fa
Linux genera un segnale solo se è abilitato con feenableexept
.
Questa è un'estensione glibc, non sono riuscito a trovare alcun modo per farlo in nessuno standard.
Quando il segnale si verifica, è perché l'hardware della CPU stesso solleva un'eccezione, che il kernel Linux ha gestito e informato l'applicazione attraverso il segnale.
Il risultato è che bash stampa Floating point exception (core dumped)
e lo stato di uscita è 136
, che corrisponde a signal 136 - 128 == 8
, che secondo:
man 7 signal
è SIGFPE
.
Nota che SIGFPE
è lo stesso segnale che otteniamo se proviamo a dividere un numero intero per 0:
int main() {
int i = 1 / 0;
}
sebbene per interi:
- dividendo qualsiasi cosa per zero si alza il segnale, poiché non c'è rappresentazione dell'infinito in numeri interi
- il segnale avviene di default, senza necessità
feenableexcept
Come gestire il SIGFPE?
Se crei solo un gestore che ritorna normalmente, porta a un ciclo infinito, perché dopo il ritorno del gestore, la divisione si ripete! Questo può essere verificato con GDB.
L'unico modo è usare setjmp
e longjmp
saltare da qualche altra parte come mostrato in: C gestire il segnale SIGFPE e continuare l'esecuzione
Quali sono alcune applicazioni del mondo reale di sNaNs?
Onestamente, non ho ancora capito un caso d'uso super utile per sNaNs, questo è stato chiesto a: Utilità di segnalazione NaN?
sNaNs si sentono particolarmente inutili perché possiamo rilevare le operazioni non valide iniziali ( 0.0f/0.0f
) che generano qNaNs con feenableexcept
: sembra che snan
sollevi solo errori per più operazioni che qnan
non sollevano per, eg ( qnan + 1.0f
).
Per esempio:
main.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);
}
compilare:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
poi:
./main.out
dà:
Floating point exception (core dumped)
e:
./main.out 1
dà:
f1 -nan
f2 -nan
Vedi anche: Come tracciare un NaN in C ++
Quali sono i flag di segnale e come vengono manipolati?
Tutto è implementato nell'hardware della CPU.
I flag risiedono in alcuni registri, così come il bit che dice se deve essere sollevata un'eccezione / un segnale.
Questi registri sono accessibili dalla userland dalla maggior parte degli archivi.
Questa parte del codice glibc 2.29 è in realtà molto facile da capire!
Ad esempio, fetestexcept
è implementato per x86_86 in sysdeps / x86_64 / fpu / ftestexcept.c :
#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)
quindi vediamo immediatamente che le istruzioni d'uso sono stmxcsr
che stanno per "Store MXCSR Register State".
Ed feenableexcept
è implementato in 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;
}
Cosa dice lo standard C su qNaN vs sNaN?
La bozza dello standard C11 N1570 afferma esplicitamente che lo standard non fa differenza tra loro in F.2.1 "Infiniti, zeri con segno e NaN":
1 Questa specifica non definisce il comportamento della segnalazione dei NaN. In genere utilizza il termine NaN per indicare NaN silenziosi. Le macro NAN e INFINITY e le funzioni nan <math.h>
forniscono designazioni per NaN e infiniti IEC 60559.
Testato in Ubuntu 18.10, GCC 8.2. GitHub a monte: