- Accueil
- TUTORIELS
- ARCHITECTURE DES ORDINATEURS
- CODAGE DES NOMBRES
- Quand le binaire ne tombe pas juste
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.
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.
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
- Jamais d'égalité sur des flottants. Comparez l'écart à une tolérance.
- 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.
- 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. - Préférez
doubleàfloatsauf contrainte de mémoire ou de bande passante. Sept chiffres sûrs s'épuisent vite. - Méfiez-vous des différences de nombres proches. Soustraire deux valeurs voisines détruit les chiffres significatifs : c'est l'élimination catastrophique.
- Sommez du plus petit au plus grand quand vous accumulez beaucoup de termes.
- 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
floatne 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
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.
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.
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.
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
Ajouter un commentaire