135 votes

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

J'ai lu des articles sur la virgule flottante et je comprends que les NaN peuvent résulter d'opérations. Mais je n'arrive pas à comprendre ce que sont exactement ces concepts. Quelle est la différence entre eux ?

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

94voto

wrdieter Points 678

Lorsqu'une opération donne lieu à un NaN discret, 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. Un NaN produira un signal, généralement sous la forme d'une exception provenant de la FPU. La levée de l'exception dépend de l'état de la FPU.

C++11 ajoute quelques éléments de langage les contrôles sur l'environnement à virgule flottante et fournit des moyens normalisés pour créer et tester les NaNs . Cependant, la mise en œuvre de ces contrôles n'est pas bien normalisée et les exceptions en virgule flottante ne sont généralement pas traitées de la même manière que les exceptions C++ standard.

Dans les systèmes POSIX/Unix, les exceptions en virgule flottante sont typiquement capturées en utilisant un gestionnaire de SIGFPE .

66voto

Ciro Santilli Points 3341

Comment les qNaNs et les sNaNs se présentent-ils expérimentalement ?

Apprenons d'abord comment identifier si nous avons un sNaN ou un qNaN.

Dans cette réponse, j'utiliserai le C++ au lieu du C, car il offre la commodité suivante std::numeric_limits::quiet_NaN y std::numeric_limits::signaling_NaN que je n'ai pas trouvé en C de manière pratique.

Je n'ai cependant pas pu trouver de fonction permettant de classer si un NaN est sNaN ou qNaN, donc nous allons simplement imprimer 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

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 la même sortie, ce qui suggère que plusieurs archs implémentent étroitement IEEE 754.

À ce stade, si vous n'êtes pas familier avec la structure des nombres à virgule flottante IEEE 754, jetez un coup d'œil à : Qu'est-ce qu'un nombre subnormal à virgule flottante ?

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 signal.

  • Les infinis sont également assez similaires avec exposant == 0xFF, mais ils ont fraction == 0.

    Pour cette raison, les NaNs doivent mettre le bit 21 à 1, sinon il ne serait pas possible de distinguer sNaN de l'infini positif !

  • nanf() produit plusieurs NaNs différents, il doit donc y avoir plusieurs encodages possibles :

    7fc00000
    7fc00001
    7fc00002

    Depuis nan0 est la même chose que std::numeric_limits<float>::quiet_NaN() nous en déduisons qu'ils sont tous des NaNs tranquilles différents.

    El C11 N1570 projet de norme confirme que nanf() génère des NaN silencieux, car nanf en avant vers strtod et 7.22.1.3 "Les fonctions strtod, strtof, et strtold" dit :

    Une séquence de caractères NAN ou NAN(n-char-sequence opt ) est interprétée comme un NaN silencieux. NaN, si elle est supportée dans le type de retour, sinon comme une partie de séquence de sujets qui n'a pas la forme attendue. la forme attendue ; la signification de la séquence n-char est définie par l'implémentation. 293)

Voir aussi :

Comment se présentent les qNaNs et les sNaNs dans les manuels ?

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

  • tout ce qui a un exposant == 0xFF et une 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 "Codages NaN dans les formats binaires" dit :

Cette sous-clause spécifie en outre les codages des NaN en tant que chaînes de bits lorsqu'ils sont les résultats d'opérations. Lorsqu'ils sont codés, tous les NaN ont un bit de signe et un motif de bits nécessaires pour identifier le codage comme étant un NaN et qui détermine son type (sNaN vs. qNaN). Les bits restants, qui se trouvent dans le champ du significande arrière codent la charge utile, qui peut être une information de diagnostic (voir ci-dessus). 34

Toutes les chaînes de bits NaN binaires ont tous les bits du champ exposant biaisé E à 1 (voir 3.4). Une chaîne de bits NaN silencieuse silencieuse doit être codée avec le premier bit (d1) du champ du significande de queue T à 1. Une chaîne de bits NaN de signalisation doit être codée de manière à ce que le premier bit du champ du significande de queue soit 0. significatif de queue est 0, un autre bit du champ significatif de queue doit être différent de zéro pour distinguer le NaN de l'infini. le NaN de l'infini. Dans le codage préféré qui vient d'être décrit, un NaN de signalisation doit être supprimé en mettant d1 à 1, laissant les autres bits de T inchangés. Pour les formats binaires, les données utiles sont codées dans les p-2 bits les moins significatifs du champ significatif de queue.

El Manuel du développeur de logiciels pour les architectures Intel 64 et IA-32 - Volume 1 Architecture de base - 253665-056US Septembre 2015 4.8.3.4 "NaNs" confirme que x86 suit la norme IEEE 754 en distinguant NaN et sNaN par le bit de fraction le plus élevé :

L'architecture IA-32 définit deux classes de NaNs : les NaNs silencieux (QNaNs) et les NaNs de signalisation (SNaNs). Un QNaN est un NaN dont le bit de la fraction la plus significative est activé et un SNaN est un NaN dont le bit de la fraction la plus significative est désactivé.

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 de 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 fractionnement 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 sont générés les qNanS et les sNaNs ?

Une différence majeure entre les qNaNs et les sNaNs est que :

  • qNaN est généré par des opérations arithmétiques régulières intégrées (logicielles ou matérielles) avec des valeurs étranges.
  • sNaN n'est jamais généré par les 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 trouvé de citations claires de l'IEEE 754 ou de la C11 pour cela, mais je n'ai pas non plus trouvé d'opération intégrée qui génère des sNaNs ;-)

Le manuel Intel énonce cependant clairement ce principe à la section 4.8.3.4 "NaNs" :

Les SNaN sont généralement utilisés pour piéger ou invoquer un gestionnaire d'exception. Ils doivent être insérés par le logiciel ; c'est-à-dire que le processeur ne génère jamais un 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 confirmer avec GDB).

Que font les qNaNs et les sNaNs différemment ?

Maintenant que nous savons à quoi ressemblent les qNaNs et les sNaNs, et comment les manipuler, nous sommes enfin prêts à essayer de faire en sorte que les sNaNs fassent leur travail et fassent sauter quelques programmes !

Donc sans plus attendre :

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 suis pas sûr qu'il existe un moyen standard d'empêcher cela.

Nous déduisons donc de cet exemple que :

  • snan + 1.0 causes FE_INVALID pero qnan + 1.0 n'est pas

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

    Il s'agit d'une extension de la glibc, je n'ai pas trouvé de moyen de faire cela dans un standard.

Lorsque le signal se produit, c'est parce que le matériel du CPU lui-même lève une exception, que le noyau Linux a traitée et a informé l'application par le biais du signal.

Le résultat est que bash imprime Floating point exception (core dumped) et l'état de sortie est 136 qui correspond à signal 136 - 128 == 8 qui, selon :

man 7 signal

est SIGFPE .

Notez que SIGFPE est le même signal que celui que nous obtenons si nous essayons de diviser un nombre entier par 0 :

int main() {
    int i = 1 / 0;
}

bien que pour les entiers :

  • diviser n'importe quoi par zéro augmente le signal, puisqu'il n'y a pas de représentation de l'infini dans les nombres entiers.
  • le signal, il se produit par défaut, sans qu'il soit nécessaire de feenableexcept

Comment gérer le SIGFPE ?

Si vous créez simplement un gestionnaire qui revient normalement, cela conduit à une boucle infinie, car après le retour du gestionnaire, la division se reproduit ! Ceci peut être vérifié avec GDB.

Le seul moyen est d'utiliser setjmp y longjmp pour sauter ailleurs comme le montre l'illustration : C gère le signal SIGFPE et continue l'exécution

Quelles sont les applications réelles des sNaNs ?

Très honnêtement, je n'ai toujours pas compris un cas d'utilisation super utile pour les sNaNs, cela a été demandé : Utilité de la signalisation de NaN ?

Les sNaNs sont particulièrement inutiles car nous pouvons détecter les opérations initiales non valides ( 0.0f/0.0f ) qui génèrent des qNaNs avec feenableexcept Il apparaît que snan soulève juste des erreurs pour plus d'opérations qui qnan ne se lève pas pour, par exemple ( qnan + 1.0f ).

Par exemple :

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

compiler :

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm

alors :

./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 mis en œuvre dans le matériel de l'unité centrale.

Les drapeaux se trouvent dans un registre, tout comme le bit qui dit si une exception ou un signal doit être soulevé.

Ces registres sont accessible depuis userland de la plupart des arcs.

Cette partie du code de la 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)

donc nous voyons immédiatement que l'utilisation des instructions est stmxcsr qui signifie "Store MXCSR Register State".

Et feenableexcept est mis en œuvre à 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 ?

El C11 N1570 projet de norme dit explicitement que la norme ne fait pas de différence entre eux à F.2.1 "Infinités, zéros signés et NaNs" :

1 Cette spécification ne définit pas le comportement des NaN de signalisation. Elle utilise généralement le terme NaN pour désigner les NaN silencieux. Les macros NAN et INFINITY, ainsi que les fonctions nan de la section <math.h> fournir des désignations pour les NaN et les infinis de la norme IEC 60559.

Testé dans Ubuntu 18.10, GCC 8.2. GitHub en amont :

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X