web-dev-qa-db-fra.com

Quelle est la différence entre NaN silencieux et NaN de signalisation?

J'ai lu sur la virgule flottante et je comprends que NaN pourrait résulter des opérations. mais je ne peux pas comprendre quels sont ces concepts exactement. Quelle est la difference?

Lequel peut être produit pendant la programmation C++? En tant que programmeur, pourrais-je écrire un programme provoquant un sNaN?

78
JalalJaberi

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 provenant de l'unité à virgule flottante (FPU) ou de la bibliothèque si la virgule flottante est implémentée dans le logiciel. Un NaN de signalisation produira un signal, généralement sous la forme d'une exception de la FPU. L'exception de l'exception dépend de l'état de la FPU.

C++ 11 ajoute quelques langages contrôle l'environnement à virgule flottante et fournit façons standardisées de créer et de tester les NaN . Cependant, si les contrôles sont implémentés n'est pas bien standardisé et les exceptions à virgule flottante ne sont généralement pas interceptées de la même manière que les exceptions C++ standard.

Dans les systèmes POSIX/Unix, les exceptions à virgule flottante sont généralement interceptées à l'aide d'un gestionnaire pour SIGFPE .

55
wrdieter

À quoi ressemblent expérimentalement les qNaN et sNaN?

Voyons d'abord comment 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 dans C commodément.

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));
}

Compiler et exécuter:

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 cela produit exactement la même sortie, ce qui suggère que plusieurs arches 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 subnormal?

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 silencieux 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, donc il doit y avoir plusieurs encodages possibles:

    7fc00000
    7fc00001
    7fc00002
    

    Puisque nan0 Est identique à std::numeric_limits<float>::quiet_NaN(), nous en déduisons que ce sont tous des NaN silencieux différents.

    Le projet standard C11 N157 confirme que nanf() génère des NaN silencieux, car nanf transmet à strtod et 7.22.1.3 "Le strtod, strtof, et les fonctions strtold "dit:

    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 de n caractères est définie par l'implémentation. 293)

Voir également:

À quoi ressemblent les qNaNs et sNaNs dans les manuels?

IEEE 754 2008 recommande que (TODO obligatoire ou facultatif?):

  • quoi que ce soit avec exposant == 0xFF et 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 "Les encodages NaN aux 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. Une fois 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 de signification de fin, codent la charge utile, qui peut être une information de diagnostic (voir ci-dessus). 34

Toutes les chaînes binaires de bits NaN ont tous les bits du champ d'exposant polarisé 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 signification de fin T étant 1. Une chaîne de bits NaN de signalisation doit être codée avec le premier bit du champ de signification de fin étant 0. Si le premier bit de la le champ de signification de fin est 0, un autre bit du champ de signification de fin doit être non nul pour distinguer le NaN de l'infini. Dans le codage préféré qui vient d'être décrit, un NaN de signalisation doit être calmé en mettant d1 à 1, en 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 significatif de fin

Le Intel 64 et IA-32 Architectures Software Developer's Manual - Volume 1 Basic Architecture - 253665-056US September 2015 4.8.3.4 "NaNs" confirme que x86 suit IEEE 754 en distinguant NaN et sNaN par la fraction la plus élevée bit:

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 réglé. Un SNaN est un NaN avec le bit de fraction le plus significatif clair.

ainsi que 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 ou un NaN de signalisation. Les deux types de NaN se distinguent par leur bit de fraction le plus significatif, 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 sont générés les qNanS et sNaN?

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 régulières (logicielles ou matérielles) 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 des programmeurs, par ex. 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 d'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 à virgule flottante.

Cela peut être vu à partir de notre exemple où les deux:

float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);

produire 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 (confirmer TODO avec GDB).

Que font les qNaNs et sNaNs différemment?

Maintenant que nous savons à quoi ressemblent les qNaN et sNaN et comment les manipuler, nous sommes enfin prêts à essayer de faire en sorte que les sNaN fassent leur travail et fassent 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 l'état 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 $?

Sortie:

FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136

Notez que ce comportement ne se produit qu'avec -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.

On déduit donc de cet exemple que:

  • snan + 1.0 Provoque FE_INVALID, Mais qnan + 1.0 Ne le fait pas

  • Linux ne génère un signal que s'il est activé avec feenableexept.

    Il s'agit d'une extension glibc, je n'ai trouvé aucun moyen de le faire dans aucune norme.

Lorsque le signal se produit, c'est parce que le matériel CPU lui-même déclenche 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, Ce qui correspond à signal 136 - 128 == 8, Qui selon:

man 7 signal

est SIGFPE.

Notez que SIGFPE 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 dans les entiers
  • le signal, il se produit 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.

La seule façon est d'utiliser setjmp et longjmp pour sauter ailleurs comme indiqué dans: 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é à: tilité de la signalisation NaN?

les sNaN se sentent particulièrement inutiles car nous pouvons détecter les opérations invalides initiales (0.0f/0.0f) qui génèrent des qNaN avec feenableexcept: il semble que snan soulève juste des erreurs pour plus d'opérations qui qnan n'augmente 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 signaux et comment sont-ils manipulés?

Tout est implémenté dans le matériel CPU.

Les drapeaux vivent dans certains registres, tout comme le bit qui dit si une exception/un signal doit être levé.

Ces registres sont accessibles depuis l'espace utilisateur de la plupart des arches.

Cette partie du code glibc 2.29 est en fait très facile à 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 les instructions utilisées sont stmxcsr qui signifie "Store MXCSR Register State".

Et feenableexcept est implémenté à 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 à propos de qNaN vs sNaN?

Le projet de norme C11 N157 dit explicitement que la norme ne les différencie pas en F.2.1 "Infinités, 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 des NaN silencieux. Les macros NAN et INFINITY et les fonctions nan dans <math.h> Fournissent des désignations pour IEC 60559 NaNs et infinis.

Testé dans Ubuntu 18.10, GCC 8.2. Amont GitHub: