IEEE 754 : on sait pas compter et on s'en fout.
Par nraynaud le vendredi, avril 3 2009, 23:16 - Développement - Lien permanent
Je viens de passer une petite semaine dans des docs à propos d'IEEE 754 et le moins qu'on puisse dire, c'est que le résultat n'est pas brillant.
Qu'est-ce que c'est ?
Il s'agit d'une norme décrivant une représentation des nombres à virgule flottante et des règles de calcul entre eux.Différentes tailles de mots
La norme définit 4 tailles de mots, avec comme règle : plus c'est petit plus c'est rapide, plus c'est grand, plus c'est précis. Un compromis plutôt intuitif. Pour tous ces mots, elle définit la manière dont on encode les valeurs dedans.Des valeurs spéciales
Afin de produire un système de calcul consistent, il n'existe pas que des nombres dans ce système.- les nombres dénormalisés : le système permet de garder encore un peu de précision même quand un nombre est trop près de zéro pour être encodé normalement
- les infinis : + et - l'infini sont représentés, et sont utilisables dans toutes les opérations, atan(+inf) donne pi/2, 1/+inf donne zéro etc.
- le zéro signé : en IEEE 754, le zéro est signé, ce qui permet de maintenir une approche "latérale" sur les fonctions qui ont une asymptote verticale en zéro, par exemple 1/+0 donne +inf
- NaN (not a number) : quand une opération est illégale, cette valeur est retournée. Par exemple 0/0 ou sqrt(-1) renvoient NaN.
Des arrondis
La norme en outre défini des arrondis, qu'on peut demander concernant le calcul. En interne, les calculs sont effectués avec un peu plus de précision que nécessaire, et au moment de couper le nombre pour le faire rentrer dans la taille de mot de l'utilisateur, il faut respecter la règle d'arrondi courante.- Vers zéro (troncature)
- Vers +infini (ceiling)
- Vers -infini (floor)
- Au plus proche (arrondi). La définition est un poil plus précise ça à cause de certains cas particulier, elle s'appelle round to even. C'est le mode par défaut.
Le calcul d'intervalle consiste à faire tourner 2 fois le calcul, une fois avec un arrondis vers l'+inf et l'autre vers -inf et on sait que la valeur réelle se situe entre les 2. On constate que si l'intervalle est grand, on a l'algorithme n'est pas robuste.
Des évènements
Lors d'un calcul, un certain nombre d'évènements peuvent se produire :- division par zéro (cet évènement est mal nommé) : il est signalé lorsqu'un infini apparait au résultat alors qu'il n'y avait pas d'infini dans l'entrée. L'exemple classique est 1/0 mais log(0) aussi devrait le produire.
- calcul inexact : cet évènement est très courant, le système passe son temps à arrondir, mais si, par hasard, il lui arrivait de ne pas massacrer un nombre, il ne devrait pas signaler cet évènement.
- opération invalide : l'exemple type est 0/0 mais aussi sqrt(-1) etc.
- overflow : assez explicite, le nombre est trop grand (positivement ou négativement) pour rentrer dans la case, même avec un chausse-pied.
- underflow : lorsqu'un nombre est trop proche de zéro et qu'il va passer en dénormalisé
- renvoyer une valeur adaptée. Par exemple 0/0 -> NaN, 1/0 -> +inf, un overflow renverra un ±inf etc.
- des flags sont disponibles pour l'utilisateur, après chaque calcul il peut les inspecter, et les remettre à zéro après les avoir vus.
- un listener d'évènement où l'utilisateur peut enregistrer son propre code qui sera exécuté à ce moment.
Un peu de positif
- pour la partie calcul, à peu près tous les langages savent compter avec des nombres
- en général, rallonger une variable (passer de 4 à 8 octets par exemple) offre bien plus de précision
Mais aucun langage ne l'implémente !
- Les développeurs de langages ne sont pas intéressés par IEEE 754.
- Le compromis précision/vitesse n'est en général pas respecté (on peut avoir plus de précision gratuitement)
- Aucun langage n'expose correctement IEEE754 à ses utilisateurs.
- La partie IEEE 754 de la norme C99 n'est pas prise en compte par gcc (face à un exemple qui fonctionnait, un développeur de gcc m'a dit : "c'est de la chance").
- Exposer les évènements de calcul sous forme de signal unix rend leur traitement très compliqué (quel thread à lancé ça ? à quel endroit exactement ?)
Des dangers
Normalement, si un NaN apparait en cours de calcul, les règles font qu'il est propagé. Mais il a un risque de ne pas être vu. En effet, les comparaisons ne propagent pas les NaN dans le calcul. Ainsi 3 < NaN est toujours faux, de même que NaN = NaN qui est toujours faux. D'ailleurs il peut pas en être autrement un booléen est un booléen même chez IEEE 754, il n'existe pas de "NaB" Not a Boolean, donc la comparaison renverra toujours une réponse, cependant, attention à son interprétation.D'autre part, en ne prenant pas soin du signe des zéros on peut faire apparaitre des infinis avec le mauvais signe, et avoir des résultats abérents.
Commentaires
"Le calcul d'intervalle consiste à faire tourner 2 fois le calcul, une fois avec un arrondis vers l'+inf et l'autre vers -inf et on sait que la valeur réelle se situe entre les 2. On constate que si l'intervalle est grand, on a l'algorithme n'est pas robuste."
Ça me semble pas aussi simple que ça, si on calcule 10/-3 - 10/3 arrondi à l'entier, d'abord vers +inf, ensuite vers -inf, dans les deux cas on trouve -7, la valeur réelle est donc censée être -7, sauf que -7 != -20/3.
oui, sur le calcul d'intervalle je me suis planté, on décide pour chaque opération dans quel sens on va envoyer l'arrondi pour avoir une borne sup et une inf.
Je me suis planté parce que j'ai jamais ni pratiqué, ni vu pratiquer.
Je me demande même si ça suffit de faire des changements de signes, avec une fonction monotone ça va, mais sinon ça peut être la cata, par exemple avec sin(x). Si x est une expression dont la "vrai" valeur est pi/2 et que les bornes inf et sup calculées sont juste avant et juste après, les bornes inf et sup de sin(x) vont toutes les deux être inférieures à la vrai valeur de sin(x).
Je ne suis pas sûr qu'il faille raisonner sur sin(x) mais plutôt sur son développement limité en 0, ce qui change un peu la donne.