Les nombres à virgule
Ce que ce chapitre apporte
- Expliquer pourquoi 0,1 n'a pas d'écriture exacte en machine, et pourquoi ce n'est pas un défaut du langage.
- Lire les trois champs d'un nombre à virgule : signe, exposant, mantisse.
- Distinguer trois choses que l'on confond : la valeur écrite, la valeur rangée, la valeur affichée.
- Situer les deux endroits où la précision se perd dans un calcul, et mesurer chacun.
- Comparer deux nombres à virgule sans utiliser ==, et choisir la tolérance.
- Reconnaître les cas où il ne faut pas de nombres à virgule du tout, et dire par quoi les remplacer.
0.1 + 0.2 ne donne pas 0.3. Cette bizarrerie est citée partout et expliquée presque nulle part, ce qui laisse une conclusion fausse et tenace, celle que l'ordinateur calculerait mal. Il calcule très bien. C'est l'entrée qui n'existe pas. Ce chapitre ouvre la mémoire et montre, chiffre par chiffre, ce qui s'y trouve à la place.
Une difficulté qui existe déjà en base dix
Personne n'est choqué d'apprendre qu'un tiers ne s'écrit pas exactement en base dix. On écrit 0,333 et on sait que ce n'est pas tout à fait un tiers. La suite des chiffres ne s'arrête jamais, alors il faut bien couper quelque part, et couper veut dire perdre.
La raison tient en une phrase : en base dix, une fraction s'écrit avec un nombre fini de chiffres si, et seulement si, son dénominateur ne contient que des facteurs 2 et 5, les diviseurs de dix. Un demi s'écrit 0,5. Un quart, 0,25. Un cinquième, 0,2. Un tiers, jamais, parce que 3 ne divise aucune puissance de dix.
En base deux, la même règle s'applique avec un seul facteur autorisé : le 2. Un demi s'écrit 0,1. Un quart, 0,01. Un huitième, 0,001. Mais un dixième demande un facteur 5 au dénominateur, et il n'y a pas de 5 dans les puissances de deux. Un dixième est donc, en base deux, exactement ce qu'un tiers est en base dix : une suite infinie et périodique.
Le programme ci-dessous développe plusieurs fractions en base deux, sans passer par les nombres à virgule de Python. Il travaille sur des fractions exactes, et s'arrête tout seul quand le développement se termine.
Les quatre premières fractions se terminent. Les trois dernières tournent en rond pour toujours. Un dixième donne 0,000110011001100110011001 et le motif 0011 se répète indéfiniment.
Ce qui est réellement rangé
Voici ce que la mémoire contient quand un programme écrit 0.1. Les bits se retournent au clic, et la valeur au-dessous suit.
- signe
- positif
- exposant
- 1019 − 1023 = -4
- nature
- normal
- hexadécimal
- 3fb999999999999a
0.100000000000000005551115123125782702118158…
La ligne à lire est celle qui s'appelle « valeur exacte en mémoire ». Elle ne vaut pas 0,1 : elle vaut
0,1000000000000000055511151231257827021181583404541015625
et ces cinquante-cinq chiffres ne sont pas une approximation de l'approximation. C'est la valeur exacte du nombre qui est dans la mémoire, écrite en entier. Elle se termine, parce qu'un nombre fait de puissances de deux a toujours une écriture décimale finie, et elle est plus longue que ce que l'on imagine, ce qui est précisément la raison pour laquelle aucun langage ne l'imprime.
Python sait la montrer, à condition de la demander.
Trois écritures du même nombre. La première est courte et trompeuse, la deuxième est tronquée, la troisième est exacte.
Decimal(0.1) reçoit un nombre à virgule déjà abîmé, et se contente d'en écrire la valeur exacte. Decimal("0.1") reçoit le texte « 0.1 », et construit le nombre exact un dixième, sans jamais passer par les bits. Les guillemets font toute la différence, et c'est sur eux que repose la dernière section de ce chapitre.
Trois champs, et rien d'autre
Un nombre à virgule est rangé comme la notation scientifique de l'école, mais en base deux. Il tient en trois morceaux :
Le signe est un seul bit : 0 pour un nombre positif, 1 pour un nombre négatif.
L'exposant dit de combien de rangs la virgule se déplace. Il est rangé décalé d'une constante, le biais, qui vaut 1023 sur 64 bits, pour qu'un exposant négatif tienne dans un champ de bits ordinaire.
La mantisse porte les chiffres du nombre, après un 1 initial qui n'est pas écrit puisqu'il est toujours là. Cette économie d'un bit s'appelle le bit implicite.
La valeur vaut alors, pour un nombre ordinaire :
Sur la figure précédente, l'exposant affiché se lit 1019 − 1023 = −4, ce qui place la valeur entre et , c'est-à-dire entre 0,0625 et 0,125. C'est bien l'intervalle où se trouve 0,1.
Trois manipulations valent mieux qu'un paragraphe. Sur la figure ci-dessus :
- Retourner le bit de signe, tout à gauche. Seul le signe change, la suite des chiffres reste identique.
- Retourner le bit de poids faible de l'exposant. La valeur est multipliée ou divisée par deux.
- Retourner le tout dernier bit de mantisse, à l'extrême droite. La valeur bouge du plus petit écart possible à cet endroit de la droite des nombres.
Ce dernier écart porte un nom, et il est la clé de tout ce qui suit : c'est le pas entre deux nombres représentables voisins. Il n'est pas constant. Près de zéro les nombres représentables sont serrés, et loin de zéro ils sont très espacés.
Autour de , le pas dépasse 1 : il n'y a plus un seul nombre représentable entre deux entiers consécutifs. Cela se vérifie directement.
Ajouter 1 à ne change rien, parce que le résultat n'existe pas et que le plus proche voisin est le point de départ.
Ce qui se range exactement, et ce qui ne se range pas
La règle énoncée plus haut se vérifie d'un coup d'œil. La colonne de droite dit si la valeur écrite dans le programme est celle qui se retrouve en mémoire.
- signe
- positif
- exposant
- 1022 − 1023 = -1
- nature
- normal
- hexadécimal
- 3fe0000000000000
0.5
| écrit | valeur exacte en mémoire | exact |
|---|---|---|
| 0.5 | 0.5 | oui |
| 0.25 | 0.25 | oui |
| 0.75 | 0.75 | oui |
| 0.125 | 0.125 | oui |
| 0.1 | 0.1000000000000000055511151231257827021181583404541015625 | non |
| 0.2 | 0.200000000000000011102230246251565404236316680908203125 | non |
| 0.3 | 0.299999999999999988897769753748434595763683319091796875 | non |
Un demi, un quart, trois quarts et un huitième passent intacts. Un dixième, deux dixièmes et trois dixièmes ne passent pas. Et il faut noter que 0,3 est rangé en dessous de sa valeur, alors que 0,1 et 0,2 sont rangés au-dessus : c'est déjà la moitié de l'explication de la section suivante.
Pourquoi 0,1 + 0,2 ne fait pas 0,3
Voici l'addition, décomposée.
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000166533453693773481063544750213623046875
0.3000000000000000444089209850062616169452667236328125
affiché par le langage : 0.30000000000000004
- à l'écriture des opérandes, avant tout calcul, si l'un des deux n'a pas d'écriture finie en base deux
- au rangement du résultat : 0.0000000000000000277555756156289135105907917022705078125
- écart total avec 0.3 : 0.0000000000000000444089209850062616169452667236328125
Il faut lire cette figure de haut en bas, parce qu'elle sépare deux pertes que tout le monde confond.
La première perte a déjà eu lieu avant la moindre addition. Ni 0,1 ni 0,2 n'ont été rangés tels qu'écrits. Leur somme exacte, celle des deux valeurs réellement en mémoire, ne vaut donc déjà pas 0,3.
La seconde perte est celle du rangement du résultat. Cette somme exacte n'est elle-même pas représentable, et elle doit être arrondie au voisin le plus proche.
Le point intéressant, et contraire à l'intuition, est l'ordre de grandeur des deux : pour cette addition-là, c'est la seconde qui pèse le plus, environ une fois et demie la première. Aucune des deux ne suffit à expliquer le phénomène, et c'est pour cela que l'explication courte, celle qui parle seulement d'un « 0,1 approximatif », laisse toujours un doute.
L'addition, elle, est parfaitement exacte : elle donne le nombre représentable le plus proche de la somme vraie de ses deux entrées. C'est ce qu'on peut demander de mieux à une machine, et c'est exactement ce que la norme lui impose.
Une conséquence à laquelle personne ne s'attend
Si chaque opération arrondit, alors changer l'ordre des opérations change le résultat. L'addition des nombres à virgule n'est pas associative.
Les mêmes trois nombres, la même opération, deux résultats différents. Ce n'est pas un bogue : chaque somme partielle est arrondie, et les arrondis ne tombent pas au même endroit selon l'ordre.
La même cause produit une dérive quand on additionne longtemps.
Dix mille additions font dériver le total d'environ , alors qu'une seule multiplication tombe juste. Moins il y a d'opérations, moins il y a d'arrondis. C'est la première règle pratique du chapitre, et elle vaut pour tous les calculs longs : sommer un million de mesures accumule un million d'arrondis.
Comparer sans le signe égal
De tout ce qui précède découle une consigne simple : ne jamais tester l'égalité de deux nombres à virgule issus de calculs différents.
math.isclose compare à une tolérance relative, dont la valeur par défaut est , soit environ un milliardième de la taille des nombres comparés. Une tolérance relative est presque toujours ce qu'il faut, parce que le pas entre représentables grandit avec les nombres : un écart acceptable près de 1 est ridicule près de .
Une exception compte : au voisinage de zéro, une tolérance relative ne veut plus rien dire, puisque tout est relativement loin de zéro. Il faut alors une tolérance absolue, que isclose accepte séparément.
while x != 1.0: x = x + 0.1 peut tourner indéfiniment, parce que la valeur 1,0 exacte n'est jamais atteinte. Il faut comparer avec < ou >, ou mieux, compter en entiers et diviser à la fin.
Sur 32 bits, tout se dégrade plus vite
Le format présenté jusqu'ici occupe 64 bits, et c'est celui que Python emploie toujours. Il en existe un autre, sur 32 bits, que l'on rencontre sur les cartes électroniques, dans les images et dans les réseaux de neurones, là où la place et la vitesse comptent plus que la précision.
- signe
- positif
- exposant
- 123 − 127 = -4
- nature
- normal
- hexadécimal
- 3dcccccd
0.100000001490116119384765625
La mantisse tombe de 52 bits à 23, et l'écriture exacte raccourcit d'autant : il reste environ sept chiffres décimaux fiables, contre seize sur 64 bits. Le phénomène est le même, l'échelle change.
Quand il ne faut pas de nombres à virgule
Tous les problèmes de ce chapitre disparaissent en changeant d'outil, et le choix dépend de ce que l'on compte.
Pour de l'argent, compter en entiers. Un prix en centimes est un entier, un entier se range exactement, et rien ne dérive.
Pour une arithmétique décimale exacte, le type Decimal. Il calcule en base dix, donc 0,1 y est exactement 0,1. Il est plus lent, et c'est un prix que toute comptabilité accepte.
Pour des fractions exactes, le type Fraction. Il garde un numérateur et un dénominateur entiers, et ne perd jamais rien, y compris sur un tiers.
Pour des mesures physiques, garder les nombres à virgule. Une température connue à un dixième de degré près n'a que faire d'une précision au millionième : l'erreur de mesure dépasse de très loin l'erreur de représentation. C'est le cas d'usage pour lequel ce format a été conçu, et il y est excellent.
Exercices type
Trouver la valeur exacte rangée pour 0,7. Utiliser Decimal, puis dire si elle est au-dessus ou en dessous de 0,7.
Compter, parmi les dixièmes de 0,1 à 0,9, combien se rangent exactement. La réponse se déduit de la règle du dénominateur, et le programme la confirme.
Mesurer la dérive d'une somme selon l'ordre des termes. Additionner mille petites valeurs et une grande, dans les deux ordres.
Quand le grand nombre vient en premier, chaque petite valeur est absorbée sans laisser de trace, parce qu'elle tombe sous le pas. Quand les petites valeurs s'additionnent entre elles d'abord, elles finissent par peser assez pour compter. Sommer du plus petit au plus grand perd moins.
Vérification
1.Pourquoi 0,1 ne se range-t-il pas exactement ?
2.Que vaut exactement le nombre rangé quand un programme écrit 0.1 ?
3.Dans 0.1 + 0.2, où la précision se perd-elle ?
4.Comment comparer deux nombres à virgule issus de calculs différents ?
5.Quel type convient pour additionner des montants en euros ?
6.Pourquoi 1e16 + 1 vaut-il encore 1e16 ?
La méthode
Devant un résultat numérique surprenant, dérouler dans cet ordre.
- Afficher la valeur exacte avec
Decimal(x), et non l'affichage par défaut. La moitié des surprises s'expliquent à cette seule ligne. - Chercher les dénominateurs. Toute fraction dont le dénominateur n'est pas une puissance de deux entre abîmée.
- Compter les opérations. Chacune arrondit. Une boucle qui additionne dix mille fois accumule dix mille arrondis.
- Remplacer toute égalité entre nombres à virgule par une comparaison à tolérance.
- Changer d'outil si le domaine l'exige. Entiers pour de l'argent,
Decimalpour du décimal exact,Fractionpour du rationnel exact, nombres à virgule pour des mesures.
Synthèse
- Une fraction s'écrit avec un nombre fini de chiffres en base deux seulement si son dénominateur est une puissance de deux. 0,1 n'en est pas une, et n'existe donc pas en machine.
- Ce qui est rangé à la place est le représentable le plus proche, dont l'écriture décimale exacte est finie mais longue : 55 chiffres pour 0,1.
- Un nombre à virgule tient en trois champs : signe, exposant biaisé, mantisse avec un 1 implicite.
- L'affichage par défaut est la plus courte écriture qui se relit à l'identique. Il cache la valeur réelle.
Decimal(x)la montre. - Dans un calcul, la précision se perd à l'écriture des opérandes et à l'arrondi du résultat. Les deux pèsent, et l'opération elle-même est exacte.
- Le pas entre représentables grandit avec les nombres : au-delà de , ajouter 1 ne change plus rien.
- L'addition n'est pas associative : changer l'ordre change le résultat. Sommer du plus petit au plus grand perd moins.
- Ne jamais comparer avec
==. Employermath.isclose, relative loin de zéro, absolue près de zéro. - Pour de l'argent, compter en entiers ou employer
Decimal. Pour du rationnel exact,Fraction. Pour des mesures physiques, les nombres à virgule conviennent parfaitement.