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.
Changer le mode d'arrondi a essentiellement 2 usages : tester la robustesse d'un algorithme ou faire du calcul d'intervalle. La robustesse d'un algorithme c'est savoir "si je change un peu mes données de départ, est-ce que ça change beaucoup mon résultat ?" si la réponse est oui, alors le calcul sera imprécis. Il s'agit de faire exactement l'inverse de Lorentz, on lance un calcul avec plusieurs modes d'arrondis et si les résultats sont trop différents, le calcul n'est pas robuste.
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é
Lors qu'un évènement survient, la norme suggère 3 types de comportements, par complexité croissante pour l'utilisateur :
  • 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.