Fiche de TD n° 3 : Codage des nombres et norme IEEE 754

Cette fiche accompagne le cours Codage des nombres en machine. Les vingt-neuf exercices suivent l'ordre des huit chapitres : on part du binaire pur et on finit sur les pièges de la virgule flottante.

La quatrième partie est la plus fournie : huit exercices sur la norme IEEE 754 en simple précision, dans les deux sens, dont un cas où la conversion ne se termine jamais et où il faut arrondir.

Chaque corrigé est masqué : cherchez d'abord, posez les calculs sur une feuille, ouvrez ensuite. Tous les motifs binaires de cette fiche ont été vérifiés par le calcul.

Codage des nombres en machine : le mot IEEE 754 de 35,5 en simple precision, decoupe en bit de signe, exposant biaise et mantisse.
29 exercices corrigésRevoir le cours

Partie 1Numération : binaire pur et hexadécimal

Conversions dans les deux sens, étendue de codage et addition binaire. De quoi se remettre en jambes avant d'aborder les nombres négatifs.

Du décimal au binaire et à l'hexadécimal

Convertissez les nombres suivants en binaire, puis en hexadécimal :

  1. 200
  2. 255
  3. 2 026
Voir le corrigéMasquer le corrigé

Corrigé

200. Par soustraction des poids : 200 − 128 = 72 ; 72 − 64 = 8 ; 8 − 8 = 0. Les poids retenus sont 128, 64 et 8.

200 = 11001000 = C8

255. C'est la plus grande valeur d'un octet : tous les bits sont à 1.

255 = 11111111 = FF

2 026. 2026 − 1024 = 1002 ; − 512 = 490 ; − 256 = 234 ; − 128 = 106 ; − 64 = 42 ; − 32 = 10 ; − 8 = 2 ; − 2 = 0.

2 026 = 11111101010 = 7EA

Pour l'hexadécimal, on complète à gauche pour obtenir des paquets de quatre : 0111 1110 1010 → 7, E, A.

De l'hexadécimal au décimal

Le mot C0DE est rangé sur 16 bits. Donnez son écriture binaire, puis sa valeur en base dix.

Voir le corrigéMasquer le corrigé

Corrigé

Chaque chiffre hexadécimal se remplace directement par quatre bits, sans aucun calcul :

C → 1100    0 → 0000    D → 1101    E → 1110

C0DE = 1100 0000 1101 1110

En base dix : 12×163 + 0×162 + 13×16 + 14 = 49 152 + 0 + 208 + 14 = 49 374.

C'est bien dans l'étendue d'un mot de 16 bits non signé, qui va de 0 à 65 535.

Combien de bits faut-il ?

Un système doit numéroter les étudiants d'une université qui en compte 48 000.

  1. Combien de bits faut-il au minimum ?
  2. Sur 16 bits non signés, reste-t-il de la marge ?
  3. Et si l'on avait choisi un entier 16 bits signé ?
Voir le corrigéMasquer le corrigé

Corrigé

1. Il faut n tel que 2n ≥ 48 000. Or 215 = 32 768 et 216 = 65 536. Il faut donc 16 bits.

2. Sur 16 bits non signés on couvre 0 à 65 535. Il reste 65 535 − 48 000 = 17 535 numéros libres. La marge existe, mais elle est mince pour une université qui grandit.

3. Un entier 16 bits signé en complément à 2 ne monte qu'à 32 767. Les numéros au-delà basculeraient dans les négatifs : le numéro 40 000 s'afficherait −25 536.

C'est une erreur de conception classique. On choisit un type signé par habitude, alors que la quantité modélisée ne peut pas être négative.

Addition en binaire pur

Posez l'addition 10110101 + 01001111 en binaire. Le résultat tient-il dans un octet ?

Voir le corrigéMasquer le corrigé

Corrigé

  1111111    ← retenues
  10110101   (181)
+ 01001111   ( 79)
----------
 100000100   (260)

Le résultat occupe neuf bits : 260 dépasse 255.

Sur une machine à registres de 8 bits, le neuvième bit sort du registre et va se loger dans l'indicateur de retenue. Le registre ne contient plus que 00000100, soit 4.

260 − 256 = 4 : le résultat est juste modulo 256

Un programme prudent teste l'indicateur de retenue après l'opération ; sans cela, l'erreur passe inaperçue.

Partie 2Les entiers signés

Binaire signé, complément à 1 et complément à 2 : coder, décoder, additionner, détecter un débordement et élargir un entier sans le déformer.

Les trois conventions sur un même nombre

Codez −96 sur 8 bits en binaire signé, en complément à 1 et en complément à 2.

Voir le corrigéMasquer le corrigé

Corrigé

On part de la valeur absolue : 96 = 64 + 32 = 01100000.

Binaire signé : on remplace le bit de poids fort par 1 et on garde les sept bits de grandeur 1100000.

−96 (bs) = 11100000

Complément à 1 : on inverse les huit bits de 01100000.

−96 (cà1) = 10011111

Complément à 2 : on ajoute 1 au précédent.

−96 (cà2) = 10100000

Vérification par la lecture directe du complément à 2 : −128 + 32 = −96. C'est juste.

Un motif, quatre lectures

Le motif 10110111 est rangé dans un octet. Donnez sa valeur en binaire pur, en binaire signé, en complément à 1 et en complément à 2.

Voir le corrigéMasquer le corrigé

Corrigé

Binaire pur : 128 + 32 + 16 + 4 + 2 + 1 = 183.

Binaire signé : signe négatif, grandeur 0110111 = 32+16+4+2+1 = 55, donc −55.

Complément à 1 : bit de poids fort à 1, on inverse pour obtenir 01001000 = 72, donc −72.

Complément à 2 : lecture directe, −128 + 32 + 16 + 4 + 2 + 1 = −73.

Quatre conventions, quatre valeurs sans rapport. C'est pourquoi un fichier binaire est illisible sans sa spécification, et pourquoi un char signé ou non signé ne donne pas le même programme.

Addition sans débordement

Calculez 73 + 45 en complément à 2 sur 8 bits. Vérifiez l'absence de débordement.

Voir le corrigéMasquer le corrigé

Corrigé

73 = 01001001 et 45 = 00101101.

  01001001   (+73)
+ 00101101   (+45)
----------
  01110110

Lecture directe : 64 + 32 + 16 + 4 + 2 = +118.

Débordement ? Les deux opérandes sont positives et le résultat est positif : la règle n'est pas déclenchée. Et 118 reste bien dans l'étendue −128 à +127.

Aucune retenue ne sort non plus : le calcul est juste pour les deux interprétations, signée et non signée.

Addition avec débordement

Calculez 100 + 45 en complément à 2 sur 8 bits. Commentez le résultat et dites quel indicateur du processeur se lève.

Voir le corrigéMasquer le corrigé

Corrigé

100 = 01100100 et 45 = 00101101.

  01100100   (+100)
+ 00101101   ( +45)
----------
  10010001

Lecture directe : −128 + 16 + 1 = −111.

Deux nombres positifs ont donné un résultat négatif : il y a débordement signé. La réponse mathématique, +145, sort de l'étendue −128 à +127.

Quel indicateur ? Le drapeau de débordement (overflow, OF ou V). En revanche aucune retenue ne sort du mot, donc le drapeau de retenue (carry, C) reste à zéro.

Si le programme avait lu ces octets comme des entiers non signés, le résultat 10010001 = 145 aurait été parfaitement correct. Le même calcul est faux dans une convention et juste dans l'autre : d'où les deux indicateurs séparés.

Soustraire sans soustracteur

Calculez 45 − 73 sur 8 bits en n'utilisant qu'un additionneur, comme le fait réellement le processeur.

Voir le corrigéMasquer le corrigé

Corrigé

Le principe : soustraire 73, c'est ajouter −73.

73 = 01001001 ; son complément à 1 est 10110110 ; plus 1 donne −73 = 10110111.

  00101101   (+45)
+ 10110111   (-73)
----------
  11100100

Lecture directe : −128 + 64 + 32 + 4 = −28.

45 − 73 = −28. C'est juste, et aucun circuit soustracteur n'a été nécessaire.

Comment le processeur s'y prend : il envoie le second opérande dans un inverseur, et il force la retenue entrante à 1. L'inversion plus la retenue entrante donnent exactement le complément à 2 : d'où une seule et même unité pour l'addition et la soustraction.

Le nombre qui n'a pas d'opposé

Sur 8 bits en complément à 2, appliquez la règle de négation à 10000000. Qu'obtenez-vous ? Quelle conséquence pour un programme ?

Voir le corrigéMasquer le corrigé

Corrigé

10000000 vaut −128 en lecture directe.

motif de depart   10000000
complement a 1    01111111
+ 1             + 00000001
                -----------
                  10000000   on retombe sur le depart

L'opposé de −128 devrait être +128, qui n'existe pas dans l'étendue −128 à +127. La négation renvoie son argument inchangé.

Conséquence. En C, -x ou abs(x) appliqué au plus petit entier du type renvoie une valeur négative. Sur int 32 bits, abs(-2147483648) vaut −2 147 483 648.

Ce n'est pas un bogue de bibliothèque : c'est la conséquence directe de l'étendue asymétrique du complément à 2. Les analyseurs statiques signalent ce cas, et il est à l'origine de failles réelles quand la valeur sert ensuite de taille de tampon.

Élargir un entier sans le déformer

Une variable 8 bits contient 11010011. On la copie dans une variable 16 bits. Donnez le résultat selon que la source est déclarée signée ou non signée.

Voir le corrigéMasquer le corrigé

Corrigé

Source non signée : le motif vaut 211. On complète par des zéros à gauche.

0000000011010011 = 211

Source signée : le motif vaut −128 + 64 + 16 + 2 + 1 = −45. On recopie le bit de signe, qui vaut 1, sur les huit positions ajoutées.

1111111111010011 = −45

Vérification : sur 16 bits, le bit de tête porte le poids −32 768. La somme des bits à 1 donne −32 768 + 32 723 = −45.

Si l'on avait complété par des zéros pour la source signée, on aurait obtenu 211 au lieu de −45. C'est pour cela que les processeurs ont deux instructions distinctes : extension par zéros et extension de signe.

Partie 3La virgule fixe

Partie fractionnaire, poids négatifs, format n.p. Et le premier nombre décimal qui refuse de tomber juste en base deux.

Conversion vers la virgule fixe

Convertissez 26,8125 en binaire à virgule fixe.

Voir le corrigéMasquer le corrigé

Corrigé

Partie entière : 26 = 16 + 8 + 2 = 11010.

Partie fractionnaire : multiplications successives par 2.

0,8125 x 2 = 1,625  ->  1   reste 0,625
0,625  x 2 = 1,25   ->  1   reste 0,25
0,25   x 2 = 0,5    ->  0   reste 0,5
0,5    x 2 = 1,0    ->  1   reste 0   on s'arrete

26,8125 = 11010,1101

Contrôle : 16 + 8 + 2 + 0,5 + 0,25 + 0,0625 = 26,8125. C'est juste.

Astuce de vérification : les quatre bits 1101 valent 13, et 13/16 = 0,8125.

Lecture d'un nombre à virgule

Que valent 1101,011 et 0,101 en base dix ?

Voir le corrigéMasquer le corrigé

Corrigé

1101,011

8 + 4 + 1 + 0,25 + 0,125 = 13,375

Détail : la partie entière 1101 vaut 13 ; la partie fractionnaire 011 vaut 0×0,5 + 1×0,25 + 1×0,125 = 0,375.

0,101

0,5 + 0 + 0,125 = 0,625

Ce second nombre reviendra dans la partie 4 : sa conversion tombe juste, ce qui en fait un bon candidat pour un codage IEEE 754 exact.

Dimensionner un format n.p

Un capteur de pression est codé en virgule fixe non signée sur 16 bits, au format 12.4.

  1. Quelle est l'étendue mesurable ?
  2. Quelle est la résolution ?
  3. Le constructeur veut désormais une résolution de 0,01 bar. Le format convient-il ?
Voir le corrigéMasquer le corrigé

Corrigé

1. La partie entière, sur 12 bits, couvre 0 à 212 − 1 = 4 095. La partie fractionnaire ajoute au plus 15/16 = 0,9375.

étendue : 0 à 4 095,9375 bar

2. Quatre bits fractionnaires donnent un pas de 1/24 = 0,0625 bar.

3. Non. 0,0625 est plus grossier que 0,01 : le capteur ne distinguerait pas 2,00 bar de 2,05 bar.

Il faut assez de bits fractionnaires pour que 2−p ≤ 0,01, soit 2p ≥ 100, donc p = 7 (pas de 0,0078125).

Sur 16 bits au total, il ne resterait que 9 bits entiers, soit une étendue réduite à 0–511 bar. C'est tout le dilemme de la virgule fixe : on échange de l'étendue contre de la précision, et le choix est figé à la conception.

Le premier nombre qui refuse de tomber juste

Essayez de convertir 0,2 en binaire. Quand vous arrêtez-vous, et pourquoi ?

Voir le corrigéMasquer le corrigé

Corrigé

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 de depart !
0,2 x 2 = 0,4  ->  0   ...

Au quatrième tour, le reste 0,2 réapparaît. Or le reste détermine entièrement la suite du calcul : les quatre mêmes opérations vont se reproduire indéfiniment.

0,2 = 0,(0011) en base deux

Pourquoi ? 0,2 = 1/5. Le dénominateur 5 n'est pas une puissance de 2 : aucune suite finie de divisions par 2 ne peut l'épuiser. En base dix, c'est exactement le même phénomène que 1/3 = 0,333…

On s'arrête donc quand on n'a plus de bits disponibles, et on arrondit. C'est l'objet de l'exercice 22.

Partie 4La norme IEEE 754 en simple précision

Le cœur de la fiche. Huit exercices de codage et de décodage sur 32 bits, dont une conversion qui boucle indéfiniment et qu'il faut arrondir à 23 bits.

Coder 45,125 en simple précision

Donnez le mot IEEE 754 de 45,125 sur 32 bits, puis son écriture hexadécimale.

Voir le corrigéMasquer le corrigé

Corrigé

Étape 1 : le signe. Positif, donc S = 0.

Étape 2 : virgule fixe. 45 = 101101 ; 0,125 = 001.

45,125 = 101101,001

Étape 3 : normalisation. Six chiffres avant la virgule, il faut la reculer de cinq rangs.

= 1,01101001 × 25, donc e = 5

Étape 4 : exposant biaisé. Eb = 5 + 127 = 132 = 10000100.

Étape 5 : mantisse. On retire le 1 implicite : il reste 01101001, que l'on complète à 23 bits par des zéros à droite.

0 10000100 01101001000000000000000

Regroupé par quatre : 0100 0010 0011 0100 1000 0000 0000 0000.

45,125 → 42 34 80 00

Coder un nombre négatif

Codez −14,5 en simple précision.

Voir le corrigéMasquer le corrigé

Corrigé

Signe : négatif, S = 1. Le reste du travail se fait sur la valeur absolue ; le signe n'intervient qu'au dernier moment.

Virgule fixe : 14 = 1110 et 0,5 = 1, donc 14,5 = 1110,1.

Normalisation : 1,1101 × 23, donc e = 3.

Exposant biaisé : Eb = 3 + 127 = 130 = 10000010.

Mantisse : 1101 suivi de dix-neuf zéros.

1 10000010 11010000000000000000000

−14,5 → C1 68 00 00

Remarquez que +14,5 donnerait exactement le même mot avec un 0 en tête, soit 41 68 00 00. En IEEE 754, changer le signe ne touche qu'un seul bit : contrairement au complément à 2.

Coder un entier rond

Codez 100 en simple précision. Combien de bits de mantisse sont réellement utilisés ?

Voir le corrigéMasquer le corrigé

Corrigé

100 = 64 + 32 + 4 = 1100100, soit sept chiffres.

Normalisation : 1,100100 × 26, donc e = 6 et Eb = 133 = 10000101.

Mantisse : 100100, complétée par des zéros. Les zéros de queue n'apportent rien : seuls les quatre premiers bits 1001 sont significatifs.

0 10000101 10010000000000000000000

100 → 42 C8 00 00

Dix-neuf bits de mantisse restent à zéro. Le nombre est donc représenté exactement : c'est le cas de tout entier jusqu'à 224 = 16 777 216.

Coder un nombre plus petit que 1

Codez 0,625 en simple précision. Quel est le signe de l'exposant réel ?

Voir le corrigéMasquer le corrigé

Corrigé

0,625 = 0,5 + 0,125 = 0,101.

Normalisation : il n'y a aucun 1 avant la virgule. Il faut l'avancer d'un rang vers la droite, ce qui retranche 1 à l'exposant.

0,625 = 1,01 × 2−1

L'exposant réel est donc négatif, ce qui est le cas de tous les nombres strictement inférieurs à 1.

Exposant biaisé : Eb = −1 + 127 = 126 = 01111110. Le biais fait son travail : le motif stocké reste positif.

0 01111110 01000000000000000000000

0,625 → 3F 20 00 00

Décoder un mot positif

Quelle valeur décimale représente 0 10000010 11001000000000000000000 ?

Voir le corrigéMasquer le corrigé

Corrigé

Signe : S = 0, le nombre est positif.

Exposant : Eb = 10000010 = 130, donc e = 130 − 127 = 3.

Mantisse : les bits stockés sont 11001 suivis de zéros. On remet le 1 implicite : la mantisse réelle est 1,11001.

Reconstruction : on décale la virgule de trois rangs vers la droite.

1,11001 × 23 = 1110,01 = 8 + 4 + 2 + 0,25

la valeur est 14,25

Piège à éviter : sans le bit implicite on aurait lu 0,11001 × 23 = 6,25 : un résultat parfaitement plausible, et faux.

Décoder un mot négatif

Quelle valeur représente 1 01111100 01000000000000000000000 ?

Voir le corrigéMasquer le corrigé

Corrigé

Signe : S = 1, le nombre est négatif.

Exposant : Eb = 01111100 = 124, donc e = 124 − 127 = −3.

Mantisse réelle : 1,01, c'est-à-dire 1 + 0,25 = 1,25.

1,25 × 2−3 = 1,25 / 8 = 0,15625

la valeur est −0,15625

Contrôle : 0,15625 = 5/32, et 5/32 s'écrit 0,00101 en binaire. En normalisant on retrouve bien 1,01 × 2−3.

Le cas qui boucle : coder 0,1

Codez 0,1 en IEEE 754 simple précision.

  1. Convertissez 0,1 en binaire. Que constatez-vous ?
  2. Normalisez et donnez l'exposant biaisé.
  3. Donnez la mantisse sur 23 bits en précisant la règle d'arrondi employée.
  4. Quelle valeur la machine stocke-t-elle réellement ?
Voir le corrigéMasquer le corrigé

Corrigé

1. La conversion ne se termine pas.

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  ← deja rencontre au 1er tour

Le reste 0,2 réapparaît : à partir de là, les quatre mêmes opérations se répètent sans fin.

0,1 = 0,0 0011 0011 0011 0011… : période 0011

La raison est arithmétique : 0,1 = 1/10 et 10 = 2×5. Le facteur 5 ne sera jamais épuisé par des divisions par deux.

2. Normalisation. Le premier 1 apparaît en quatrième position après la virgule : il faut avancer la virgule de quatre rangs.

0,1 = 1,1001 1001 1001… × 2−4

Eb = −4 + 127 = 123 = 01111011.

3. La mantisse et l'arrondi. On écrit les bits après la virgule et on compte jusqu'à 23.

bits 1 a 23 : 1001 1001 1001 1001 1001 100
bit 24 et suivants : 1100 1100 ...
                     ^
            le 24e bit vaut 1 et la suite n'est pas nulle
            -> on arrondit au superieur : on ajoute 1 au 23e bit

Le 23e bit passe de 0 à 1.

M = 10011001100110011001101

La norme impose l'arrondi au plus proche, avec égalité vers le nombre pair. C'est ce qui garantit qu'un même calcul donne le même résultat sur toutes les machines.

4. Le mot complet et la valeur réelle.

0 01111011 10011001100110011001101
soit 3D CC CC CD en hexadecimal

En relisant ce mot exactement, on obtient :

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

L'écart avec 0,1 vaut environ 1,5×10−9. Il est minuscule ; il suffit pourtant à faire échouer tout test d'égalité. 0,1 n'existe pas en machine : seul existe le flottant le plus proche.

Coder 2 026 et mesurer la marge

Codez 2 026 en simple précision. Jusqu'à quel entier ce format reste-t-il exact ?

Voir le corrigéMasquer le corrigé

Corrigé

2 026 = 11111101010, soit onze chiffres.

Normalisation : 1,1111101010 × 210, donc e = 10 et Eb = 137 = 10001001.

0 10001001 11111010100000000000000

2 026 → 44 FD 40 00

Dix bits de mantisse sont utilisés sur 23 : il en reste treize libres.

Jusqu'où reste-t-on exact ? Un entier s'écrit exactement tant que sa représentation binaire tient sur 24 bits significatifs : 23 stockés plus le bit implicite.

tout entier jusqu'à 224 = 16 777 216 est exact

Au-delà, un entier sur deux seulement est représentable, puis un sur quatre, et ainsi de suite. Voir l'exercice 26.

Partie 5Double précision, cas spéciaux et pièges

Le format 64 bits, les motifs réservés (zéro, infini, NaN), la perte de précision et ce qu'il faut en déduire quand on programme.

Le même nombre en double précision

Codez 35,5 en double précision (64 bits) et comparez au codage en simple précision.

Voir le corrigéMasquer le corrigé

Corrigé

La méthode est identique ; seules les tailles et le biais changent : 11 bits d'exposant, 52 de mantisse, biais 1023.

Signe : S = 0.

Normalisation : 35,5 = 100011,1 = 1,000111 × 25, donc e = 5.

Exposant biaisé : Eb = 5 + 1023 = 1028 = 10000000100 sur 11 bits.

Mantisse : 000111 suivi de quarante-six zéros.

simple : 0 10000100 00011100000000000000000
         42 0E 00 00

double : 0 10000000100 0001110000000000000000000000...
         40 41 C0 00 00 00 00 00

Les bits significatifs sont les mêmes : 35,5 est exact dans les deux formats, la double précision n'apporte rien ici. Elle ne sert que lorsque la mantisse déborde, c'est-à-dire dès que le nombre ne tombe pas juste.

Les motifs réservés

Donnez les mots de 32 bits correspondant à +0, −0, +∞ et NaN. Comment un programme détecte-t-il un NaN ?

Voir le corrigéMasquer le corrigé

Corrigé

La norme réserve les deux valeurs extrêmes de l'exposant : 0 et 255.

+0    : 0 00000000 00000000000000000000000
-0    : 1 00000000 00000000000000000000000
+inf  : 0 11111111 00000000000000000000000
-inf  : 1 11111111 00000000000000000000000
NaN   : . 11111111 xxxxxxxxxxxxxxxxxxxxxxx  (mantisse non nulle)

Les deux zéros. Le bit de signe est indépendant de la grandeur : +0 et −0 ont des motifs différents. La norme impose toutefois que la comparaison +0 == -0 réponde vrai.

Détecter un NaN. NaN est le seul nombre différent de lui-même. Le test est donc :

if (x != x) { /* x est un NaN */ }

Les bibliothèques fournissent isnan(x), qui fait exactement cela. Toute comparaison impliquant un NaN, y compris < et >, renvoie faux, ce qui peut casser un tri si l'on n'y prend pas garde.

Le pas qui double

En simple précision :

  1. Quel est l'écart entre deux valeurs consécutives dans [1 ; 2] ? Dans [1024 ; 2048] ?
  2. Les mots de 16 777 216 et de 16 777 217 sont-ils différents ?
Voir le corrigéMasquer le corrigé

Corrigé

1. Dans [2k ; 2k+1], le dernier bit de mantisse porte le poids 2k×2−23.

  • Dans [1 ; 2], k = 0 : le pas vaut 2−23 ≈ 1,19×10−7.
  • Dans [1024 ; 2048], k = 10 : le pas vaut 2−13 ≈ 1,22×10−4, soit 1 024 fois plus grossier.

Chaque intervalle contient pourtant le même nombre de valeurs : 223 = 8 388 608. C'est sa largeur qui double.

2. Non, les deux mots sont identiques.

16 777 216 = 2^24  ->  4B 80 00 00
16 777 217         ->  4B 80 00 00   le meme !

À partir de 224, le pas vaut 2 puis 4, puis 8… Un float ne distingue plus deux entiers consécutifs. Un identifiant d'utilisateur ou un montant rangé dans un float commence à perdre des valeurs bien avant que le programme ne s'en aperçoive.

L'absorption

En simple précision, on exécute :

float x = 100000000.0f;   // 1e8
float y = x + 1.0f;
float z = y - x;

Que vaut z ? Expliquez.

Voir le corrigéMasquer le corrigé

Corrigé

z vaut 0, et non 1.

Pourquoi. 108 est compris entre 226 = 67 108 864 et 227 = 134 217 728. Dans cet intervalle, le pas vaut 226−23 = 8.

Le résultat exact de x + 1 est 100 000 001, qui n'est pas représentable. Le flottant le plus proche est… 100 000 000 lui-même. L'arrondi ramène donc y à x, et la différence est nulle.

On dit que le 1 a été absorbé par le grand nombre.

Conséquence pratique. Dans une boucle qui accumule un million de petites valeurs, les dernières cessent de compter dès que le total devient grand devant elles. Deux parades : sommer du plus petit au plus grand, ou employer une sommation compensée (algorithme de Kahan).

0,1 + 0,2 et la comparaison d'égalité

En double précision, le test 0.1 + 0.2 == 0.3 renvoie faux.

  1. Expliquez pourquoi, sans parler de bogue.
  2. Écrivez le test correct.
  3. Une boucle for (x = 0; x != 1; x += 0.1) s'arrête-t-elle ?
Voir le corrigéMasquer le corrigé

Corrigé

1. Aucun des trois nombres n'est représentable exactement : ni 0,1, ni 0,2, ni 0,3. Chacun est remplacé par le flottant le plus proche.

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

La somme des deux arrondis ne tombe pas sur le même motif que l'arrondi de 0,3. Chaque opération a pourtant donné le flottant le plus proche du résultat exact : aucune n'est fautive. C'est leur enchaînement qui décale.

2. On compare l'écart à une tolérance :

if (fabs((0.1 + 0.2) - 0.3) < 1e-9) { ... }

La tolérance se choisit selon l'ordre de grandeur des nombres comparés ; pour des valeurs autour de 1 en double précision, 10−9 est raisonnable.

3. Non, la boucle ne s'arrête jamais. Après dix tours, x vaut 0,9999999999999999 et non 1 ; le test d'inégalité reste vrai et la variable continue de croître.

Règle : un compteur de boucle se compte en entiers. On écrit for (i = 0; i < 10; i++) et l'on calcule x = i / 10.0 à l'intérieur.

Des francs CFA dans un flottant

Une application de caisse stocke les montants en francs CFA dans un float. Le comptable constate des écarts de quelques francs sur les cumuls de fin de mois.

  1. À partir de quel montant le problème apparaît-il ?
  2. Passer en double suffit-il ?
  3. Que faut-il faire ?
Voir le corrigéMasquer le corrigé

Corrigé

1. Un float représente exactement tous les entiers jusqu'à 224 = 16 777 216. Au-delà, le pas vaut 2, puis 4, puis 8…

Dès qu'un cumul dépasse environ 16,8 millions de FCFA, deux montants qui diffèrent d'un franc deviennent indiscernables. Les additions suivantes arrondissent, et les petites sommes finissent absorbées par le total.

2. Un double recule le seuil à 253 ≈ 9×1015, ce qui met à l'abri pour des montants en francs entiers. Mais le problème de fond demeure : dès qu'une monnaie a des centimes ou qu'on applique un taux (TVA, remise, change), les valeurs cessent de tomber juste et les arrondis reviennent.

3. La bonne pratique : ne jamais compter l'argent en flottant.

  • Stocker un entier d'unités 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, stocker le nombre de centimes.
  • Ou employer un type décimal : DECIMAL en SQL, BigDecimal en Java, decimal en C# ou en Python.
  • Arrondir explicitement, au moment précis où la réglementation l'impose, et pas avant.

Ce n'est pas une précaution théorique : c'est la règle que suivent tous les systèmes comptables et bancaires.

Questions / Réponses

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
Aucune note. Soyez le premier à attribuer une note !

Ajouter un commentaire

Anti-spam