À quoi ressemblent les qNaN et les sNaN expérimentalement?
Apprenons d'abord à identifier si nous avons un sNaN ou un qNaN.
J'utiliserai C ++ dans cette réponse au lieu de C car il offre la commodité std::numeric_limits::quiet_NaN
et std::numeric_limits::signaling_NaN
que je n'ai pas pu trouver en C de manière pratique.
Je n'ai cependant pas pu trouver de fonction pour classer si un NaN est sNaN ou qNaN, alors imprimons simplement les octets bruts 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));
}
Compilez et exécutez:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
sortie sur ma machine x86_64:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Nous pouvons également exécuter le programme sur aarch64 avec le mode utilisateur 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
et qui produit exactement le même résultat, suggérant que plusieurs arcades implémentent étroitement IEEE 754.
À ce stade, si vous n'êtes pas familier avec la structure des nombres à virgule flottante IEEE 754, jetez un œil à: Qu'est-ce qu'un nombre à virgule flottante sous-normal?
En binaire, certaines des valeurs ci-dessus sont:
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
De cette expérience, nous observons que:
qNaN et sNaN semblent être différenciés uniquement par le bit 22: 1 signifie calme et 0 signifie signalisation
Les infinis sont également assez similaires avec l'exposant == 0xFF, mais ils ont une fraction == 0.
Pour cette raison, NaNs doit mettre le bit 21 à 1, sinon il ne serait pas possible de distinguer sNaN de l'infini positif!
nanf()
produit plusieurs NaN différents, il doit donc y avoir plusieurs encodages possibles:
7fc00000
7fc00001
7fc00002
Puisque nan0
c'est le même que std::numeric_limits<float>::quiet_NaN()
, nous en déduisons qu'ils sont tous des NaN silencieux différents.
Le projet standard C11 N1570 confirme que nanf()
génère des NaN silencieux, car nanf
les fonctions strtod
forward to et 7.22.1.3 "Les fonctions strtod, strtof et strtold" indiquent:
Une séquence de caractères NAN ou NAN (n-char-sequence opt) est interprétée comme un NaN silencieux, s'il est pris en charge dans le type de retour, sinon comme une partie de séquence sujet qui n'a pas la forme attendue; la signification de la séquence n-char est définie par l'implémentation. 293)
Voir également:
À quoi ressemblent les qNaN et les sNaN dans les manuels?
IEEE 754 2008 recommande que (TODO obligatoire ou facultatif?):
- tout ce qui a l'exposant == 0xFF et la fraction! = 0 est un NaN
- et que le bit de fraction le plus élevé différencie qNaN de sNaN
mais il ne semble pas dire quel bit est préféré pour différencier l'infini de NaN.
6.2.1 "Encodages NaN dans des formats binaires" dit:
Ce paragraphe spécifie en outre les codages des NaN sous forme de chaînes de bits lorsqu'ils sont le résultat d'opérations. Lorsqu'ils sont codés, tous les NaN ont un bit de signe et un modèle de bits nécessaires pour identifier le codage en tant que NaN et qui détermine son type (sNaN vs qNaN). Les bits restants, qui se trouvent dans le champ significand de fin, codent la charge utile, qui pourrait être des informations de diagnostic (voir ci-dessus). 34
Toutes les chaînes binaires de bits NaN ont tous les bits du champ d'exposant biaisé E mis à 1 (voir 3.4). Une chaîne de bits NaN silencieuse doit être codée avec le premier bit (d1) du champ de significand de fin T étant 1. Une chaîne de bits de signalisation NaN doit être codée avec le premier bit du champ de significand de fin étant 0. Si le premier bit du Le champ de significande de fin est 0, un autre bit du champ de significande de fin doit être différent de zéro pour distinguer le NaN de l'infini. Dans le codage préféré qui vient d'être décrit, une signalisation NaN doit être atténuée en mettant d1 à 1, laissant les bits restants de T inchangés. Pour les formats binaires, la charge utile est codée dans les p − 2 bits les moins significatifs du champ de significande de fin
Le Intel 64 et IA-32 Architectures Logicielles Manuel de développeur - Volume 1 Basic Architecture - Septembre 253665-056US ici à 2015 4.8.3.4 "Nans" confirme que x86 suit IEEE 754 en distinguant NaN et SNAN par le bit de fraction la plus élevée:
L'architecture IA-32 définit deux classes de NaN: les NaN silencieux (QNaN) et les NaN de signalisation (SNaN). Un QNaN est un NaN avec le bit de fraction le plus significatif défini et un SNaN est un NaN avec le bit de fraction le plus significatif clair.
et il en va de même pour le Manuel de référence de l'architecture ARM - ARMv8, pour le profil d'architecture ARMv8-A - DDI 0487C.a A1.4.3 "Format à virgule flottante simple précision":
fraction != 0
: La valeur est un NaN et est soit un NaN silencieux, soit un NaN de signalisation. Les deux types de NaN se distinguent par leur bit de fraction le plus significatif, le bit [22]:
bit[22] == 0
: Le NaN est un NaN de signalisation. Le bit de signe peut prendre n'importe quelle valeur et les bits de fraction restants peuvent prendre n'importe quelle valeur sauf tous les zéros.
bit[22] == 1
: Le NaN est un NaN silencieux. Le bit de signe et les bits de fraction restants peuvent prendre n'importe quelle valeur.
Comment les qNanS et les sNaN sont-ils générés?
Une différence majeure entre les qNaN et les sNaN est que:
- qNaN est généré par des opérations arithmétiques intégrées (logicielles ou matérielles) régulières avec des valeurs étranges
- sNaN n'est jamais généré par des opérations intégrées, il ne peut être ajouté explicitement que par les programmeurs, par exemple avec
std::numeric_limits::signaling_NaN
Je n'ai pas pu trouver de citations claires IEEE 754 ou C11 pour cela, mais je ne peux pas non plus trouver d'opération intégrée qui génère des sNaN ;-)
Le manuel Intel énonce cependant clairement ce principe à 4.8.3.4 "NaNs":
Les SNaN sont généralement utilisés pour intercepter ou appeler un gestionnaire d'exceptions. Ils doivent être insérés par logiciel; c'est-à-dire que le processeur ne génère jamais de SNaN à la suite d'une opération en virgule flottante.
Cela peut être vu dans notre exemple où les deux:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
produisent exactement les mêmes bits que std::numeric_limits<float>::quiet_NaN()
.
Ces deux opérations se compilent en une seule instruction d'assemblage x86 qui génère le qNaN directement dans le matériel (TODO confirme avec GDB).
Que font différemment les qNaN et les sNaN?
Maintenant que nous savons à quoi ressemblent les qNaN et les sNaN, et comment les manipuler, nous sommes enfin prêts à essayer de faire faire leur travail aux sNaN et à faire exploser certains programmes!
Alors sans plus tarder:
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;
}
Compilez, exécutez et obtenez le statut de sortie:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Production:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Notez que ce comportement ne se produit que -O0
dans GCC 8.2: avec -O3
, GCC pré-calcule et optimise toutes nos opérations sNaN! Je ne sais pas s'il existe un moyen conforme aux normes d'empêcher cela.
Nous déduisons donc de cet exemple que:
snan + 1.0
provoque FE_INVALID
, mais qnan + 1.0
ne fait pas
Linux ne génère un signal que s'il est activé avec feenableexept
.
Ceci est une extension de la glibc, je n'ai trouvé aucun moyen de le faire dans aucun standard.
Lorsque le signal se produit, c'est parce que le matériel du processeur lui-même lève une exception, que le noyau Linux a gérée et a informé l'application via le signal.
Le résultat est que bash imprime Floating point exception (core dumped)
, et l'état de sortie est 136
, qui correspond au signal 136 - 128 == 8
, qui selon:
man 7 signal
est SIGFPE
.
Notez que SIGFPE
c'est le même signal que nous obtenons si nous essayons de diviser un entier par 0:
int main() {
int i = 1 / 0;
}
bien que pour les entiers:
- diviser quoi que ce soit par zéro augmente le signal, car il n'y a pas de représentation à l'infini en nombres entiers
- le signal, il arrive par défaut, sans avoir besoin de
feenableexcept
Comment gérer le SIGFPE?
Si vous créez simplement un gestionnaire qui retourne normalement, cela conduit à une boucle infinie, car après le retour du gestionnaire, la division se produit à nouveau! Cela peut être vérifié avec GDB.
Le seul moyen est d'utiliser setjmp
et longjmp
de sauter ailleurs comme indiqué à: C gérer le signal SIGFPE et continuer l'exécution
Quelles sont les applications réelles des sNaN?
Honnêtement, je n'ai toujours pas compris un cas d'utilisation super utile pour les sNaN, cela a été demandé à: Utilité de la signalisation NaN?
Les sNaNs se sentent particulièrement inutiles car nous pouvons détecter les opérations initiales invalides ( 0.0f/0.0f
) qui génèrent des qNaNs avec feenableexcept
: il semble que cela snan
soulève simplement des erreurs pour plus d'opérations qui qnan
ne lèvent pas pour, par exemple ( qnan + 1.0f
).
Par exemple:
principal 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);
}
compiler:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
puis:
./main.out
donne:
Floating point exception (core dumped)
et:
./main.out 1
donne:
f1 -nan
f2 -nan
Voir aussi: Comment tracer un NaN en C ++
Quels sont les drapeaux de signalisation et comment sont-ils manipulés?
Tout est implémenté dans le matériel du CPU.
Les drapeaux vivent dans un registre, tout comme le bit qui indique si une exception / un signal doit être levé.
Ces registres sont accessibles depuis le userland depuis la plupart des archs.
Cette partie du code de la glibc 2.29 est en fait très simple à comprendre!
Par exemple, fetestexcept
est implémenté pour x86_86 à 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)
nous voyons donc immédiatement que le mode d'emploi est celui stmxcsr
qui signifie "Store MXCSR Register State".
Et feenableexcept
est implémenté dans 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;
}
Que dit la norme C sur qNaN vs sNaN?
Le projet de norme C11 N1570 dit explicitement que la norme ne fait pas de différence entre eux à F.2.1 "Infinis, zéros signés et NaN":
1 Cette spécification ne définit pas le comportement des NaN de signalisation. Il utilise généralement le terme NaN pour désigner les NaN silencieux. Les macros NAN et INFINITY et les fonctions nan dans <math.h>
fournissent des désignations pour les NaN et les infinis CEI 60559.
Testé dans Ubuntu 18.10, GCC 8.2. GitHub en amont: