CHAPITRE 8 : QUAND LE BINAIRE NE TOMBE PAS JUSTE

Ouvrez une console Python et tapez 0.1 + 0.2. La réponse n'est pas 0,3, mais 0,30000000000000004. Ce n'est pas un bogue de Python : la même chose se produit en C, en Java, en JavaScript, dans un tableur. C'est une conséquence directe de tout ce que nous avons vu, et ce dernier chapitre explique exactement d'où elle vient.

Le développement qui ne s'arrête jamais

Reprenons la méthode du chapitre 5 pour convertir 0,1 en binaire.

Tableau des cinq premieres multiplications par deux pour convertir 0,1 en binaire. Au cinquieme tour le reste 0,2 reapparait, ce qui signale que la suite des bits se repete indefiniment.

0,1 x 2 = 0,2  ->  0   reste 0,2
0,2 x 2 = 0,4  ->  0   reste 0,4
0,4 x 2 = 0,8  ->  0   reste 0,8
0,8 x 2 = 1,6  ->  1   reste 0,6
0,6 x 2 = 1,2  ->  1   reste 0,2   ← le reste 0,2 est deja passe

0,1 = 0,0 0011 0011 0011 ...  en base 2

Au cinquième tour, le reste 0,2 réapparaît. Or le reste détermine entièrement la suite du calcul : la machine va donc refaire exactement les mêmes quatre opérations, indéfiniment. Les bits 0011 se répètent sans fin.

Ce n'est pas une faiblesse de l'ordinateur, c'est une propriété arithmétique. 0,1 vaut 1/10, et 10 = 2×5. Le facteur 5 ne peut jamais être épuisé par des divisions par 2.

La règle générale. Une fraction irréductible p/q a une écriture binaire finie si et seulement si q est une puissance de 2. En base dix, 1/3 ne tombe pas juste parce que 3 ne divise pas 10 ; en base deux, 1/10 ne tombe pas juste parce que 5 ne divise pas 2.

Les fractions sympathiques en binaire sont donc les demis, quarts, huitièmes : 0,5 ; 0,25 ; 0,125 ; 0,625 ; 0,8125. Les dixièmes et les centièmes, c'est-à-dire presque tous les prix et toutes les mesures du monde réel, ne tombent jamais juste.

Que range vraiment la machine ?

La mantisse ne fait que 23 bits. Il faut donc couper, et arrondir au plus proche. Voici le codage de 0,1 en simple précision :

0,1 = 0,0 0011 0011 0011 0011 0011 0011 ...
    = 1,1001 1001 1001 1001 1001 1001 ... x 2^-4

Eb = -4 + 127 = 123 = 01111011

mantisse brute  : 10011001100110011001100|1100...
                                          ^ le 24e bit vaut 1
                                            on arrondit au superieur

mantisse stockee: 10011001100110011001101

resultat : 0 01111011 10011001100110011001101
           soit 3D CC CC CD en hexadecimal

Le nombre réellement stocké n'est donc pas 0,1 mais :

13 421 773 / 134 217 728 = 0,100000001490116…

L'écart vaut environ 1,5×10−9. C'est minuscule, et c'est très exactement ce qui fera échouer un test d'égalité.

D'où vient le 0,30000000000000004

Dans un double, 0,1 et 0,2 sont chacun arrondis. Leur somme est arrondie à son tour. Et le résultat de ces trois arrondis successifs ne tombe pas sur le même motif que l'arrondi de 0,3.

0,1 stocke -> 0,1000000000000000055511151...
0,2 stocke -> 0,2000000000000000111022302...
leur somme -> 0,3000000000000000444089209...
0,3 stocke -> 0,2999999999999999888977697...

0,1 + 0,2 == 0,3  ->  FAUX

Chaque étape est correcte : le processeur donne à chaque fois le flottant le plus proche du résultat exact. C'est l'accumulation qui décale, et aucune opération n'a « mal » travaillé.

Ne testez jamais l'égalité de deux flottants. Comparez leur écart à une tolérance : |a − b| < ε.

Le choix de ε dépend de l'ordre de grandeur des nombres comparés ; pour des valeurs autour de 1 en double précision, 10−9 est un ordre de grandeur raisonnable.

La densité : tous les écarts ne se valent pas

Deuxième surprise, moins connue que la première mais plus dangereuse. Les nombres représentables ne sont pas répartis régulièrement sur la droite réelle.

Droite des nombres de 1 a 32. Chaque intervalle entre deux puissances de deux contient le meme nombre de valeurs representables, mais il est deux fois plus large que le precedent, donc le pas double a chaque octave.

Chaque intervalle [2k ; 2k+1] contient exactement le même nombre de valeurs : 223 = 8 388 608 en simple précision. Mais l'intervalle est deux fois plus large que le précédent. Donc le pas double à chaque puissance de deux.

Conséquence spectaculaire : en simple précision, au-delà de 224 = 16 777 216, le pas devient supérieur à 1. Deux entiers consécutifs ne sont plus distinguables.

en simple precision :
   16 777 216  ->  4B 80 00 00
   16 777 217  ->  4B 80 00 00    le meme motif !

L'absorption

Le même phénomène sous une autre forme. Prenons, toujours en simple précision :

x = 100 000 000,0      (1e8)
y = x + 1              ->  100 000 000,0
z = y - x              ->  0,0

on a ajoute 1, puis repris 1, et on obtient 0
au lieu de 1

Autour de 108, le pas en simple précision vaut environ 8. Ajouter 1 ne change aucun bit : le résultat exact est arrondi sur le même flottant. Le petit nombre est absorbé par le grand.

C'est le piège classique des boucles d'accumulation : si l'on somme un million de petites valeurs dans un total qui grandit, les dernières ne comptent plus. La parade consiste à sommer du plus petit au plus grand, ou à employer un algorithme de sommation compensée.

Sept règles pour programmer sans mauvaise surprise

  1. Jamais d'égalité sur des flottants. Comparez l'écart à une tolérance.
  2. Jamais de flottant pour de l'argent. Comptez en unités entières indivisibles, en francs CFA, en centimes, en millièmes, avec un entier 64 bits ou un type décimal.
  3. Jamais de flottant comme compteur de boucle. for (x = 0; x != 1; x += 0.1) ne s'arrête pas : en double, la somme de dix fois 0,1 vaut 0,9999999999999999.
  4. Préférez double à float sauf contrainte de mémoire ou de bande passante. Sept chiffres sûrs s'épuisent vite.
  5. Méfiez-vous des différences de nombres proches. Soustraire deux valeurs voisines détruit les chiffres significatifs : c'est l'élimination catastrophique.
  6. Sommez du plus petit au plus grand quand vous accumulez beaucoup de termes.
  7. Ne concluez pas qu'un calcul est faux parce qu'il affiche 0,30000000000000004. Le calcul est juste ; c'est la question qui était mal posée.

Pour finir

Rien de tout cela n'est un défaut de l'ordinateur. La norme IEEE 754 est un compromis d'ingénierie remarquable : elle fait tenir dans 32 bits une étendue de plus de soixante-dix ordres de grandeur, avec sept chiffres significatifs garantis, et des opérations qui s'exécutent en une poignée de cycles.

Le prix à payer est que l'ensemble des nombres représentables est fini, alors que les nombres réels sont infinis, et infiniment denses. Toute la difficulté du calcul numérique tient dans cet écart. Le programmeur qui sait d'où il vient écrit du code qui résiste ; celui qui l'ignore écrit du code qui marche sur ses jeux d'essai et se trompe en production.

Ce qu'il faut retenir

  • Une fraction a une écriture binaire finie seulement si son dénominateur est une puissance de 2. 0,1 ne tombe jamais juste.
  • Le développement de 0,1 boucle avec la période 0011 : on coupe à 23 bits et on arrondit.
  • 0,1 stocké en simple précision vaut 0,10000000149… ; l'écart suffit à faire échouer un test d'égalité.
  • Le pas double à chaque puissance de 2 : au-delà de 224, un float ne distingue plus deux entiers consécutifs.
  • Absorption : ajouter un petit nombre à un grand peut ne rien changer.
  • Jamais d'égalité, jamais d'argent, jamais de compteur de boucle en flottant.

Exercices d'application

Exercice 1

Montrez que 0,7 ne s'écrit pas exactement en binaire, et donnez sa période.

Voir le corrigéMasquer le corrigé
0,7 x 2 = 1,4  ->  1   reste 0,4
0,4 x 2 = 0,8  ->  0   reste 0,8
0,8 x 2 = 1,6  ->  1   reste 0,6
0,6 x 2 = 1,2  ->  1   reste 0,2
0,2 x 2 = 0,4  ->  0   reste 0,4  ← deja vu

Le reste 0,4 réapparaît au cinquième tour : la suite 0110 se répète indéfiniment.

0,7 = 0,1(0110) en base deux

Explication arithmétique : 0,7 = 7/10, et le dénominateur 10 contient le facteur 5, qu'aucune division par 2 ne peut éliminer.

Exercice 2

En simple précision, quel est le pas entre deux valeurs consécutives dans l'intervalle [8 ; 16] ? Et dans [1 ; 2] ?

Voir le corrigéMasquer le corrigé

Dans [1 ; 2], l'exposant vaut 0 et le dernier bit de mantisse porte le poids 2−23. Le pas est donc 2−23 ≈ 1,19×10−7.

Dans [8 ; 16], l'exposant vaut 3. Tout est multiplié par 23 = 8 : le pas vaut 23×2−23 = 2−20 ≈ 9,5×10−7.

Règle générale : dans [2k ; 2k+1], le pas vaut 2k−23. Il double à chaque intervalle.

Exercice 3

Un programme calcule une moyenne en ajoutant 0,1 à un total, dix fois de suite, dans une variable double. Le test total == 1.0 réussit-il ?

Voir le corrigéMasquer le corrigé

Non. Chaque addition arrondit, et les écarts s'accumulent dans le même sens. Le total vaut 0,9999999999999999, pas 1.

L'écart est d'environ 10−16, soit un cent millionième de milliardième, et il suffit à faire échouer le test, puisqu'une égalité ne tolère aucun écart.

Deux corrections possibles : tester fabs(total - 1.0) < 1e-9, ou mieux, compter en entiers et diviser à la toute fin.

Exercice 4

Expliquez pourquoi un système de caisse qui stockerait les montants en francs CFA dans un float finirait par produire des écarts, et dites ce qu'il faut faire à la place.

Voir le corrigéMasquer le corrigé

Un float garantit environ sept chiffres décimaux significatifs. Au-delà de 16 777 216, le pas dépasse 1 franc : deux montants qui diffèrent d'un franc deviennent indiscernables.

Un chiffre d'affaires cumulé de quelques dizaines de millions de FCFA, tout à fait ordinaire, entre déjà dans cette zone. Les additions successives arrondissent, et l'écart grandit à mesure que le total monte, par absorption des petites sommes.

La bonne pratique : compter en unités entières indivisibles. Le franc CFA n'ayant pas de subdivision en usage, un entier 64 bits suffit et reste exact jusqu'à plus de neuf trillions. Pour une monnaie à centimes, on stocke le nombre de centimes, ou l'on emploie un type décimal (DECIMAL en SQL, BigDecimal en Java).

Posez le calcul sur une feuille avant d'ouvrir le corrigé : le codage s'apprend au crayon, pas en lecture.

Questions / Réponses

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
2 votes. Moyenne 5 sur 5.

Ajouter un commentaire

Anti-spam