Lorsqu'une opération aboutit à un NaN silencieux, rien n'indique que quelque chose est inhabituel jusqu'à ce que le programme vérifie le résultat et voit un NaN. Autrement dit, le calcul se poursuit sans aucun signal de l'unité à virgule flottante (FPU) ou de la bibliothèque si la virgule flottante est implémentée dans le logiciel. Une signalisation NaN produira un signal, généralement sous la forme d'une exception de la FPU. Le déclenchement de l'exception dépend de l'état du FPU.
C ++ 11 ajoute quelques contrôles de langage sur l'environnement en virgule flottante et fournit des méthodes standardisées pour créer et tester les NaN . Cependant, l'implémentation des contrôles n'est pas bien standardisée et les exceptions à virgule flottante ne sont généralement pas capturées de la même manière que les exceptions C ++ standard.
Dans les systèmes POSIX / Unix, les exceptions en virgule flottante sont généralement capturées à l'aide d'un gestionnaire pour SIGFPE .
À 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
etstd::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
Compilez et exécutez:
sortie sur ma machine x86_64:
Nous pouvons également exécuter le programme sur aarch64 avec le mode utilisateur QEMU:
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:
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:Puisque
nan0
c'est le même questd::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, carnanf
les fonctionsstrtod
forward to et 7.22.1.3 "Les fonctions strtod, strtof et strtold" indiquent:Voir également:
À quoi ressemblent les qNaN et les sNaN dans les manuels?
IEEE 754 2008 recommande que (TODO obligatoire ou facultatif?):
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:
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:
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:
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":
Cela peut être vu dans notre exemple où les deux:
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
Compilez, exécutez et obtenez le statut de sortie:
Production:
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
provoqueFE_INVALID
, maisqnan + 1.0
ne fait pasLinux 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 est136
, qui correspond au signal136 - 128 == 8
, qui selon:est
SIGFPE
.Notez que
SIGFPE
c'est le même signal que nous obtenons si nous essayons de diviser un entier par 0:bien que pour les entiers:
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
etlongjmp
de sauter ailleurs comme indiqué à: C gérer le signal SIGFPE et continuer l'exécutionQuelles 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 avecfeenableexcept
: il semble que celasnan
soulève simplement des erreurs pour plus d'opérations quiqnan
ne lèvent pas pour, par exemple (qnan + 1.0f
).Par exemple:
principal c
compiler:
puis:
donne:
et:
donne:
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 :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 :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":
Testé dans Ubuntu 18.10, GCC 8.2. GitHub en amont:
la source