Les nombres à virgule flottante

| 3 Comments | 11 minutes read

Beaucoup de développeurs ne sont pas très à l’aise avec les nombres à virgule flottante. Voici un petit article montrant par l’exemple quelques subtilités à leur sujet. On ne va pas expliquer en détail comment ils fonctionnent, tout est très clair sur wikipedia, on fera juste un petit rappel. Les entiers sont composés d’une somme de puissances positives de 2 (1, 2,4,8,16…). Les nombres à virgule flottante utilisent une notation scientifique mantisse * 2exposant. La mantisse est composée de puissances négatives de 2 (2-1= ½ , ¼ …). Mantisse et exposant se partagent les bits de la valeur. Nous allons nous concentrer sur les conséquences concrètes de ce fonctionnement particulier.

On va commencer par quelques curiosités.

Distribution

Contrairement aux nombres entier, les nombres à virgule flottante ne sont pas répartis de manière homogène. Il y a beaucoup plus de petits nombres que de grands.

  • La moitié d’entre eux sont positifs, les autres sont négatifs (on s’en doutait).
  • La moitié d’entre eux ont une valeur absolue inférieure à 1, les autres sont supérieurs.
  • Pour chaque valeur de l’exposant, les nombres sont répartis de manière linéaire, on perd en précision chaque fois que l’exposant est incrémenté.

L’affichage est presque toujours une approximation

À quelques exceptions près, les valeurs représentées à l’écran d’un float (ou double) sont des approximations de la valeur réellement contenue en mémoire. Tout simplement parce que la plupart des nombres ne sont pas représentables en base 10.

Les décimaux exacts sont rares

Essayez ! Utilisez la console Javascript de votre navigateur (touche F12) et saisissez

0.1 + 0.2 <entrée>

résultat : 0.30000000000000004 …

En fait, seules les puissances négatives de 2 et leurs sommes sont exactes ½=0.5, ¼=0.25, ½+¼=0.75…

L’associativité n’est pas assurée

Selon l’ordre du calcul, le résultat sera différent

var x=0.1;
var y=0.3;
var z=0.00001;
(x+y)+z==(z+x)+y; // false 

À partir de grandes valeurs, il n’est plus possible d’incrémenter un float

Essayez ceci dans la console :

9007199254740992 + 1

Le résultat est 9007199254740992 car la précision pour ce grand nombre est supérieure à l’unité, le nombre suivant est 9007199254740994, soit deux unités supplémentaires. Cette limite est propre aux double (64 bits) qu’utilise Javascript.

Le résultat d’un algorithme dépend du processeur utilisé

Le même programme utilisant des nombres à virgule flottante sur différents processeurs pourra donner des résultats différents. Même si les processeurs respectent les règles d’arrondi du standard IEEE_754.

 

Pourquoi ? (click ici)

Il y a de nombreuses raisons. Pour certains types d’opération telles que le MAC (multiply-accumulate) le processeur peut utiliser une variable intermédiaire dont la précision n’est pas toujours la même. Aussi, dans certains cas, le préprocesseur, celui qui réordonne les opérations pour pouvoir les exécuter plus vite, va inverser certaines opérations et donc en changer légèrement le résultat.

 

Certains nombres à virgule flottante sont spéciaux

Il existe un 0+ et un 0-, +infinity et –infinity mais aussi NaN (Not A Number) qui apparaît après une opération impossible telle que la division par 0 ou la racine carrée d’un nombre négatif.
L’information NaN est codée dans l’exposant et donc il en existe autant que peut représenter la mantisse. Ces différentes versions de NaN sont parfois utilisées pour différencier les opérations à l’origine de l’erreur.

Classés dans l’ordre au niveau binaire

Au niveau binaire, les nombres à virgule flottante se comparent comme des entiers (hors valeurs spéciales).

Pourquoi ? (click ici)

L’exposant est représenté de manière décalée il contiendra une valeur comprise entre 0 et 2048 pour représenter l’intervalle -1023 à +1024, cela permet d’éliminer le signe et donc de comparer comme des entiers au niveau binaire.

 

Les nombres dénormalisés ne sont pas pris en charge pas le CPU

Enfin, pas de manière purement hardware en tout cas. Un nombre dénormalisé est un nombre dont l’exposant est minimum, ce sont donc des nombres très petits (~=10-309). Quand un processeur rencontre ce type de nombre, il stope le fil d’execution et lance une petite routine pour réaliser l’opération désirée en applicant une série d’instructions binaires. Cette opération est environ 200 fois plus lente que pour un nombre normal. Sur les languages bas niveau, il est possible de considérer ces nombres comme nul en activant les flags DAZ et FTZ du CPU.

En pratique qu’en fait-on ?

Quand les utiliser ?

N’utiliser les nombres à virgule flottante que pour enregistrer des valeurs de mesures scientifiques : une température, une distance, une masse, une vitesse, une accélération… Vérifiez si leur précision convient dans les cas extrêmes et surtout ne croyez pas qu’ils sont magiques.

Ne pas utiliser de nombres à virgule flottante pour enregistrer des valeurs devant être exactes. Par exemple il ne faut pas les utiliser pour les valeurs monétaires. On peut par contre utiliser des entiers et raisonner en centimes, ou des décimaux conçus pour représenter exactement les nombres en base 10.

Ne pas accumuler les erreurs de calcul ou d’arrondi à l’affichage

Eviter d’accumuler les erreurs de calculs. On doit toujours penser à minimiser le nombre d’opérations entre les données sources et le résultat de l’algorithme.

Par exemple pour créer une animation d’un objet 3D qui tourne sur lui-même, on pourrait à chaque image reprendre le modèle 3D résultant de l’image précédente et lui appliquer une rotation de 1°. En utilisant cette méthode, l’objet se déformerait à chaque transformation et très rapidement vous aurez une patate. La bonne méthode est de toujours utiliser les données de l’objet original et de calculer sa version transformée par rotation d’un angle évoluant à chaque image 1° pour la première image, puis 2°, etc….

Accumulation d’erreurs d’affichage. J’ai récemment développé pour une petite application CAO une fonction calculant la réparation à intervalles réguliers de poutres sous un plancher. En mémoire, les valeurs semblent justes, mais quand on additionne les largeurs de chaque poutre et leur espacement, on obtenait parfois une longueur totale supérieure à celle du plancher demandé.

Utiliser une valeur epsilon pour les comparaisons

Comme nous l’avons vu plus haut, une comparaison peut ne pas avoir le résultat escompté.

var x=0.1;
var y=0.3;
var z=0.00001;
(x+y)+z==(z+x)+y; // false 

Pour éviter ce problème, on va utiliser une constante epsilon très petite.

Remplacez

if(a==b)

par

if(Math.abs(a-b)<EPSILON)

Le choix de cette valeur EPSILON n’est pas évident, une valeur comme 0.00001 conviendra pour les petits nombres mais sera inutile pour les grands. Je recommanderais donc de choisir une valeur qui correspond au domaine métier, il faut qu’elle soit suffisamment faible pour être négligeable dans le domaine métier, mais suffisamment élevée pour être supérieure aux imprécisions de calcul.

Par exemple, un micron pour une application de menuiserie. Quelques centimètres pour une application de calcul de distance à l’échelle nationale.

Dans le cas où le domaine métier n’est pas défini, il est nécessaire d’utiliser un EPSILON relatif à la valeur absolue des nombres comparés. Pour définir ce nombre il est possible d’utiliser une division mais ce n’est pas efficace, on préférera une opération binaire qui annule n bits de poids fort (et en faire une macro inline).

Se méfier de la représentation en texte

J’ai rencontré un bug sur une application de vente en ligne réalisée en PHP (on m’a forcé). Une transaction sur 500 faisait bugger le système de paiement. Le bug a été très complexe a reproduire en environnement de test, car une transaction simulée du même montant passait sans soucis. Après des heures de recherche j’ai découvert que PHP construisait une requête SQL avec une représentation scientifique du nombre (1.234E2) et que cette notation n’était pas supportée par MySQL. Le problème c’est que ce bug n’apparaissait que si l’on suivait exactement le processus de calcul de la valeur avec des multiplications et additions des produits de la commande. Il était donc impossible de réaliser le test unitaire en entrant la valeur problématique telle qu’elle était vue dans les logs de transaction.

Quelle précision ?

Il est important de vérifier que la précision correspond bien à l’usage qu’on en fait. Prenons un exemple : une application nécessite de stocker la position d’un objet connecté sur l’équateur.

Si l’on prend un point de référence, la distance possible dans chaque direction est :

6 371km * PI = 20 015 km = 20 015 000m

Si on utilise un entier 32 bits, sa précision est uniforme sur l’ensemble des valeurs à l’unité près, nous devons choisir à quoi correspond cette unité. Pour cela, il suffit de diviser la distance maximale par le nombre maximal supporté par un entier 32bit.

20 015 000m / 2 147 483 648 valeurs possibles = 0.009 m par unité ~= 1cm

Nous pourrons donc stocker la position à 1cm près où que l’objet soit situé. Faisons maintenant un essai avec un nombre à virgule flottante 32 bits. On peut considérer pour commencer que chaque unité du nombre représente la même unité de longueur qu’on a trouvé précédemment, le cm. Au centre on aura une précision de 2-23 cm soit un nanomètre, ce qui devrait suffire à priori. A l’extrémité, on devra enregistrer un nombre proche de 2 001 500 000. Nous sommes donc sur un ordre de grandeur 230 (Log2(2 001 500 000)=30), or comme la précision de la mantisse est de 223 les opérations seront arrondies à 27 unités soit 128 cm.

Reste à évaluer si ce niveau de précision convient pour l’application. Sinon il est possible d’utiliser une unité plus grande comme le mètre. On sera cent fois moins précis à l’origine (100 nanomètre), mais plus à l’extrémité.

Cette vérification est importante.

Autre exemple, certains développeurs de jeux vidéo utilisent un float pour stocker le temps écoulé depuis le début du jeu. L’un d’entre eux, Forest Smith a réalisé ce tableau :

Ordre de
grandeur
Durée écouléePrecision
du nombre
Precision
de la durée
11 seconde1.19E-07119 nanosecondes
1010 secondes9.54E-07.954 microseconde
100~1.5 minutes7.63E-067.63 microsecondes
1,000~16 minutes6.10E-0561.0 microsecondes
10,000~3 heures0.000977.976 millisecondes
100,000~1 jours0.007817.81 millisecondes
1,000,000~11 jours0.062562.5 millisecondes
10,000,000~4 mois11 seconde
100,000,000~3 années88 secondes
1,000,000,000~32 années6464 secondes

 

En voici un autre pour les distances

Ordre de grandeurOrdre de grandeur de la longueurPrécision du nombrePrécision en longeurEquivalent concret
11 mètre1.19E-07119 nanomètresvirus
1010 mètres9.54E-07.954 micromètresBacterie e. coli
100100 mètres7.63E-067.63 micromètresGlobule rouge
1,0001 kilomètre6.10E-0561.0 micromètresDiamètre d’un cheveu
10,00010 kilomètres0.000977.976 millimètresEpaisseur d’un trombone
100,000100 kilomètres0.007817.81 millimètresUne fourmis
1,000,000.16x diamètre de la terre0.062562.5 millimètresCarte de crédit
10,000,0001.6x diamètre de la terre11 mètre17 ricards chez tonton
100,000,000.14x diamètre du soleil88 mètres4 Chewbaccas
1,000,000,0001.4x diamètre du soleil6464 mètresDemi terrain de foot

Quid du javascript ?

Javascript ne permet pas de choisir le format de donnée de votre nombre. Le moteur le fait automatiquement pour vous. Si vous assignez une valeur entière à une variable, il utilisera un entier (int), si vous le multipliez par un grand nombre au point qu’il déborde de sa capacité, le moteur verra l’exception overflow et utilisera alors un nombre à virgule flottante.

Vous pouvez forcer le moteur à utiliser des entiers en utilisant le code |0 après chaque affectation. C’est de l’ASMJs.

a = b + c |0 ;

C’est particulièrement utile si la valeur de a est ensuite utilisée pour accéder à un élément d’un tableau.

Le gain de performance est aussi conséquent.

Conclusion

Vous avez maintenant des informations concrètes sur l’utilisation des nombres à virgule flottante.

Si un de vos collaborateurs propose d’utiliser un nombre à virgule flottante pour stocker une valeur monétaire ou pire une clé primaire – ne riez pas j’en ai vu -, vous pouvez déjà lui faire lire cet article, et si il insiste vous pouvez l’euthanasier de ma part.

Sébastien Caunes Author: Sébastien Caunes

Couteau suisse du numérique.

Curieux de tout et du reste.

Développeur multimédia et web, passionné de hardware et software, parce que l'un ne va pas sans l'autre. Intégriste de l'optimisation, parce qu'en plus d'être fun, c'est écologique.

Ma phrase : "Quand est-ce qu'on va rider ?"

Like it?  Share  it!

Share Button

3 Comments

  1. Cool article, on l’oublie souvent, mais il faut toujours garder ce genre de chose à l’esprit, sinon, vivent les insomnies passées à ne rien comprendre, et à breaker dans tous les sens

  2. Superbe article !
    Maintenant, j’ai une raison beaucoup plus valable d’être passé d’un float vers un int sur un traitement monétaire. À la base, j’ai opéré ce changement car sur Mac la virgule est une virgule, et sur PC c’est un point… La flemme peut faire faire des choses utiles des fois 😅

  3. Bravo ! Article génial ! Je me souviens avoir galéré sur un bug avec du code JS qui manipulait des identifiants Facebook : à un certain moment une librairie se disait “tiens, cette chaîne ressemble à un nombre, convertissons-la en nombre”. Et boum, la valeur représentable la plus proche était supérieure de 2 unités…

    Et j’espère que tous les développeurs Node.js qui font du calcul financier en JS vont se dire après avoir lu cet article que ce n’est vraiment pas une bonne idée, sauf à faire très très attention.

What do  You  think? Write a comment!

Leave a Reply

Required fields are marked *.


CommentLuv badge