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

97

J'ai lu sur la virgule flottante et je comprends que NaN pourrait résulter des opérations. Mais je ne peux pas comprendre exactement ce que sont ces concepts. Quelle est la différence entre eux?

Lequel peut être produit lors de la programmation C ++? En tant que programmeur, pourrais-je écrire un programme qui provoque un sNaN?

JalalJaberi
la source

Réponses:

68

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 .

écrivain
la source
34
Ajout à ceci: Généralement, le but d'un signalement NaN (sNaN) est le débogage. Par exemple, les objets à virgule flottante peuvent être initialisés à sNaN. Ensuite, si le programme échoue à l'un d'eux une valeur avant de l'utiliser, une exception se produira lorsque le programme utilise le sNaN dans une opération arithmétique. Un programme ne produira pas de sNaN par inadvertance; aucune opération normale ne produit de sNaN. Ils ne sont créés que dans le but d'avoir une signalisation NaN, et non à la suite d'une quelconque arithmétique.
Eric Postpischil
18
En revanche, les NaN sont destinés à une programmation plus normale. Ils peuvent être produits par des opérations normales lorsqu'il n'y a pas de résultat numérique (par exemple, en prenant la racine carrée d'un nombre négatif lorsque le résultat doit être réel). Leur but est généralement de permettre à l'arithmétique de se dérouler normalement. Par exemple, vous pouvez avoir une vaste gamme de nombres, dont certains représentent des cas spéciaux qui ne peuvent pas être traités normalement. Vous pouvez appeler une fonction compliquée pour traiter ce tableau, et elle pourrait fonctionner sur le tableau avec l'arithmétique habituelle, en ignorant les NaN. Après la fin, vous sépareriez les cas spéciaux pour plus de travail.
Eric Postpischil
@wrdieter Merci, alors seule une différence majeure génère une exception ou non.
JalalJaberi
@EricPostpischil Merci pour votre attention sur la deuxième question.
JalalJaberi
@JalalJaberi oui, l'exception est la principale différence
Wrdieter
35

À 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_NaNet std::numeric_limits::signaling_NaNque 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 nan0c'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 nanfles fonctions strtodforward 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 -O0dans 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.0provoque FE_INVALID, mais qnan + 1.0ne 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 SIGFPEc'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 setjmpet longjmpde 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 snansoulève simplement des erreurs pour plus d'opérations qui qnanne 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, fetestexceptest 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 stmxcsrqui signifie "Store MXCSR Register State".

Et feenableexceptest 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:

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
en.wikipedia.org/wiki/IEEE_754#Interchange_formats souligne que IEEE-754 suggère simplement que 0 pour signaler NaNs est un bon choix d'implémentation, pour permettre de calmer un NaN sans risquer d'en faire un infini (significand = 0). Apparemment, ce n'est pas standardisé, bien que ce soit ce que fait x86. (Et le fait que ce soit le MSB du significand qui détermine qNaN vs sNaN est standardisé). fr.wikipedia.org/wiki/Single-precision_floating-point_format dit que x86 et ARM sont les mêmes, mais PA-RISC a fait le choix opposé.
Peter Cordes
@PeterCordes oui, je ne suis pas sûr de ce que le "devrait" == "doit" ou "est préféré" dans IEEE 754 20at "Une chaîne de bits de signalisation NaN doit être codée avec le premier bit du champ significand final étant 0".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
re: mais il ne semble pas spécifier quel bit doit être utilisé pour différencier l'infini de NaN. Vous avez écrit cela comme vous vous attendiez à ce qu'il y ait un élément spécifique que la norme recommande de définir pour distinguer sNaN de l'infini. IDK pourquoi vous vous attendez à ce qu'il y ait un tel bit; tout choix non nul est bien. Choisissez simplement quelque chose qui identifie plus tard d'où vient le sNaN. IDK, sonne juste comme un phrasé étrange, et ma première impression en le lisant était que vous disiez que la page Web ne décrivait pas ce qui distingue inf de NaN dans l'encodage (un signifiant tout à zéro).
Peter Cordes
Avant 2008, IEEE 754 disait quel est le bit de signalisation / silencieux (bit 22) mais pas quelle valeur spécifiait quoi. La plupart des processeurs avaient convergé vers 1 = silencieux, ce qui a donc été intégré à la norme dans l'édition 2008. Il dit «devrait» plutôt que «doit» pour éviter de rendre les anciennes implémentations qui rendaient le même choix non conforme. En général, «devrait» dans une norme signifie «doit, à moins que vous n'ayez des raisons très impérieuses (et de préférence bien documentées) de ne pas vous conformer».
John Cowan le