Utilité de la signalisation NaN?

J’ai récemment lu pas mal de choses sur l’IEEE 754 et l’architecture x87. Je pensais utiliser NaN comme “valeur manquante” dans un code de calcul numérique sur lequel je travaille, et j’espérais que l’utilisation de la signalisation NaN me permettrait de détecter une exception en virgule flottante dans les cas où je ne veux pas procéder avec “valeurs manquantes.” Inversement, j’utiliserais NaN silencieux pour permettre à la “valeur manquante” de se propager à travers un calcul. Cependant, la signalisation NaN ne fonctionne pas comme je le pensais basée sur la documentation (très limitée) qui existe sur eux.

Voici un résumé de ce que je sais (tout cela en utilisant x87 et VC ++):

  • _EM_INVALID (l’exception “non valide” IEEE) contrôle le comportement du x87 lorsqu’il rencontre des NaN
  • Si _EM_INVALID est masqué (l’exception est désactivée), aucune exception n’est générée et les opérations peuvent renvoyer NaN silencieux. Une opération impliquant la signalisation NaN ne provoquera pas une exception, mais sera convertie en NaN silencieux.
  • Si _EM_INVALID est démasqué (exception activée), une opération non valide (par exemple, sqrt (-1)) provoque la levée d’une exception non valide.
  • Le x87 ne génère jamais de NaN de signalisation.
  • Si _EM_INVALID est démasqué, toute utilisation d’un NaN de signalisation (même l’initialisation d’une variable avec celle-ci) provoque la levée d’une exception non valide.

La bibliothèque standard permet d’accéder aux valeurs NaN:

std::numeric_limits::signaling_NaN(); 

et

 std::numeric_limits::quiet_NaN(); 

Le problème est que je ne vois aucune utilité pour la signalisation NaN. Si _EM_INVALID est masqué, il se comporte exactement comme un NaN silencieux. Aucun NaN n’étant comparable à un autre NaN, il n’y a pas de différence logique.

Si _EM_INVALID n’est pas masqué (l’exception est activée), on ne peut même pas initialiser une variable avec un NaN de signalisation: double dVal = std::numeric_limits::signaling_NaN(); car cela génère une exception (la valeur NaN de signalisation est chargée dans un registre x87 pour la stocker dans l’adresse mémoire).

Vous pouvez penser comme suit:

  1. Masque _EM_INVALID.
  2. Initialiser la variable avec la signalisation NaN.
  3. Unmask_EM_INVALID.

Cependant, l’étape 2 entraîne la conversion du NaN de signalisation en un NaN silencieux, les utilisations ultérieures ne provoqueront donc pas d’ exceptions! Alors WTF?!

Y a-t-il une utilité ou un but quelconque à un NaN de signalisation? Je crois comprendre que l’une des intentions initiales était d’initialiser la mémoire pour que l’utilisation d’une valeur à virgule flottante unifiée puisse être interceptée.

Est-ce que quelqu’un peut me dire si je manque quelque chose ici?


MODIFIER:

Pour illustrer davantage ce que j’avais espéré faire, voici un exemple:

Envisagez d’effectuer des opérations mathématiques sur un vecteur de données (doubles). Pour certaines opérations, je souhaite que le vecteur contienne une “valeur manquante” (par exemple, cela correspond à une colonne de feuille de calcul dans laquelle certaines cellules n’ont pas de valeur, mais leur existence est significative). Pour certaines opérations, je ne souhaite pas que le vecteur contienne une “valeur manquante”. Je voudrais peut-être prendre une autre mesure si une “valeur manquante” est présente dans l’ensemble – peut-être effectuer une opération différente (ce n’est donc pas un état invalide).

Ce code original ressemblerait à ceci:

 const double MISSING_VALUE = 1.3579246e123; using std::vector; vector missingAllowed(1000000, MISSING_VALUE); vector missingNotAllowed(1000000, MISSING_VALUE); // ... populate missingAllowed and missingNotAllowed with (user) data... for (vector::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) { if (*it != MISSING_VALUE) *it = sqrt(*it); // sqrt() could be any operation } for (vector::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) { if (*it != MISSING_VALUE) *it = sqrt(*it); else *it = 0; } 

Notez que la vérification de la “valeur manquante” doit être effectuée à chaque itération de boucle . Bien que je comprenne dans la plupart des cas, la fonction sqrt (ou toute autre opération mathématique) va probablement occulter cette vérification, il y a des cas où l’opération est minime (peut-être juste une addition) et la vérification est coûteuse. Sans parler du fait que la “valeur manquante” prend le pas sur une valeur d’entrée légale et pourrait provoquer des bugs si un calcul parvient légitimement à cette valeur (si peu probable que cela puisse être). Aussi, pour être techniquement correct, les données saisies par l’utilisateur doivent être comparées à cette valeur et une action appropriée doit être entreprise. Je trouve cette solution inélégante et peu performante. Ceci est un code critique pour les performances, et nous n’avons certainement pas le luxe de structures de données parallèles ou d’objects d’éléments de données.

La version NaN ressemblerait à ceci:

 using std::vector; vector missingAllowed(1000000, std::numeric_limits::quiet_NaN()); vector missingNotAllowed(1000000, std::numeric_limits::signaling_NaN()); // ... populate missingAllowed and missingNotAllowed with (user) data... for (vector::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) { *it = sqrt(*it); // if *it == QNaN then sqrt(*it) == QNaN } for (vector::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) { try { *it = sqrt(*it); } catch (FPInvalidException&) { // assuming _seh_translator set up *it = 0; } } 

Maintenant, la vérification explicite est éliminée et les performances doivent être améliorées. Je pense que tout cela fonctionnerait si je pouvais initialiser le vecteur sans toucher les registres FPU …

De plus, j’imagine que toute implémentation de sqrt respecte doit rechercher NaN et renvoie NaN immédiatement.

Si je comprends bien, le but de la signalisation NaN est d’initialiser les structures de données, mais, bien sûr, l’initialisation d’ exécution en C risque d’avoir le NaN chargé dans un registre flottant dans le cadre de l’initialisation. pas conscient que cette valeur flottante doit être copiée à l’aide d’un registre entier.

J’espère que vous pourriez initialiser une valeur static avec un NaN de signalisation, mais même cela nécessiterait un traitement spécial par le compilateur pour éviter de le convertir en NaN silencieux. Vous pourriez peut-être utiliser un peu de magie pour éviter de la traiter comme une valeur flottante lors de l’initialisation.

Si vous écriviez dans ASM, ce ne serait pas un problème. mais en C et surtout en C ++, je pense que vous devrez subvertir le système de types pour initialiser une variable avec NaN. Je suggère d’utiliser memcpy .

Utiliser des valeurs spéciales (même NULL) peut rendre vos données beaucoup plus confuses et votre code beaucoup plus compliqué. Il serait impossible de faire la distinction entre un résultat QNaN et une valeur QNaN “spéciale”.

Vous pourriez mieux conserver une structure de données parallèle pour suivre la validité, ou peut-être avoir vos données FP dans une structure de données différente (fragmentée) pour ne conserver que des données valides.

C’est un conseil assez général; Les valeurs spéciales sont très utiles dans certains cas (par exemple, contraintes de mémoire ou de performances très ssortingctes), mais à mesure que le contexte augmente, elles peuvent causer plus de difficultés qu’elles ne le méritent.

Ne pourriez-vous pas simplement avoir une constante 64_t où les bits ont été mis à ceux d’un nan de signalisation? tant que vous le traitez comme un type entier, le nan de signalisation n’est pas différent des autres entiers. Vous pouvez l’écrire où vous voulez grâce au pointer-casting:

 Const uint64_t sNan = 0xfff0000000000000; Double[] myData; ... Uint64* copier = (uint64_t*) &myData[index]; *copier=sNan | myErrorFlags; 

Pour plus d’informations sur les bits à définir: https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html