Les entiers sont exacts, mais leur étendue est étroite : un entier de 64 bits s’arrête vers 9,2 × 10¹⁸, et n’a rien entre 0 et 1. La science et l’ingénierie ont besoin, dans le même programme, de nombres comme la constante d’Avogadro, 6,022 × 10²³, et la constante de Planck, 6,626 × 10⁻³⁴. Les écrire tous deux en virgule fixe demanderait plus de 50 chiffres, pour la plupart des zéros sans signification. La virgule flottante résout le problème comme les scientifiques sur papier : on stocke quelques chiffres significatifs, plus un exposant qui dit où placer la virgule.
Ce chapitre construit l’idée à partir de zéro et convertit quelques nombres à la main. Le chapitre sur les types de données couvre le côté matériel (le tableau des formats, le fp16 et le bfloat16, pourquoi 0,1 + 0,2 ne vaut pas 0,3), et les deux chapitres se complètent.
La notation scientifique
En notation scientifique, un nombre s’écrit comme une mantisse (significand) multipliée par une puissance de dix : 6,022 × 10²³. Le nombre de chiffres de la mantisse fixe la précision ; la taille de l’exposant fixe l’étendue. Les deux sont indépendantes, et c’est tout l’intérêt.
Imaginez un format décimal jouet avec une mantisse de 3 chiffres et un exposant de −99 à 99. Il peut contenir des nombres de 1,00 × 10⁻⁹⁹ à 9,99 × 10⁹⁹, mais seulement trois chiffres significatifs de chacun. Entre 1,23 × 10⁵ et le nombre suivant, 1,24 × 10⁵, il y a un écart de 1 000. Entre 1,23 × 10⁻² et 1,24 × 10⁻², l’écart est de 0,0001. Les écarts grandissent avec les nombres, mais, rapportés au nombre, ils restent à peu près de la même taille : un peu moins de 1 %. C’est le compromis qui définit la virgule flottante : une précision relative constante sur une étendue énorme.
Un nombre est normalisé quand son premier chiffre significatif n’est pas nul : 1,23 × 10⁵, et non 0,123 × 10⁶ ou 12,3 × 10⁴. La normalisation donne à chaque nombre une représentation unique.
La virgule flottante binaire
Les ordinateurs font la même chose en base 2 : un nombre vaut ± mantisse × 2^exposant, la mantisse étant écrite en binaire. En binaire, une mantisse normalisée commence toujours par 1, le seul chiffre binaire non nul : elle s’écrit donc toujours 1,quelque chose. Puisque ce 1 de tête est toujours là, inutile de le stocker. Ce bit caché offre un bit de précision gratuit.
La norme IEEE 754, publiée en 1985 et suivie depuis par pratiquement tous les CPU, fixe la disposition. Un float de 32 bits (binary32) contient :
| Champ | Bits | Contient |
|---|---|---|
| signe | 1 | 0 pour +, 1 pour − |
| exposant | 8 | l’exposant + 127 (excédent 127, voir le chapitre sur le complément à deux) |
| fraction | 23 | les bits après la virgule dans 1,fraction |
Un double de 64 bits (binary64) a 1, 11 et 52 bits, avec un biais de 1023. La valeur d’un nombre normal est :
value = (−1)^sign × 1.fraction × 2^(exponent − bias)
L’exposant est biaisé plutôt qu’en complément à deux pour que, signe mis à part, les flottants plus grands aient des motifs de bits plus grands : les flottants positifs se comparent correctement comme de simples entiers.
Convertir 5,75 à la main
- Passer en binaire. 5 s’écrit
101. 0,75 vaut ½ + ¼, donc.11. 5,75 =101.11₂. - Normaliser. Déplacer la virgule de deux rangs vers la gauche :
1.0111× 2². - Signe : positif, donc 0.
- Exposant : 2 + 127 = 129 = 1000 0001.
- Fraction : les bits après le 1 de tête, complétés à 23 bits : 0111 0000 0000 0000 0000 000.
Assemblé :
0 10000001 01110000000000000000000
= 0100 0000 1011 1000 0000 0000 0000 0000
= 0x40B80000
L’explorateur ci-dessous stocke le nombre. Vérifiez les champs par rapport aux étapes ci-dessus, puis cliquez sur des bits pour voir comment chacun change la valeur :
À essayer : Tapez un nombre ou cliquez sur un exemple, changez de format (en haut à droite), ou cliquez sur un bit pour l’inverser.
- sign
- 0 → positive
- exponent
- 129 − 127 = 2
- significand
- 1.fraction = 1.4375
- value
- +1.4375 × 2^2 = 5.75
Rounding is round-to-nearest, ties-to-even, as in hardware. Sums like 0.1+0.2 are computed in double precision first, as a program would, then stored in the chosen format.
Un exemple négatif : −0,15625
- Passer en binaire. 0,15625 = 5/32 = ⅛ + 1/32 =
0.00101₂. - Normaliser. Déplacer la virgule de trois rangs vers la droite :
1.01× 2⁻³. - Signe : négatif, donc 1.
- Exposant : −3 + 127 = 124 = 0111 1100.
- Fraction : 0100 0000 0000 0000 0000 000.
1 01111100 01000000000000000000000 = 0xBE200000
Les deux nombres sont exacts : leur développement binaire se termine. Tapez -0.15625 dans l’explorateur ci-dessus pour vérifier. Les nombres comme 0,1, dont le développement binaire se répète indéfiniment, doivent être tronqués, ce qui nous amène à l’arrondi.
L’arrondi
Entre deux flottants voisins, il y a un écart. Sa taille s’appelle un ulp (unit in the last place, unité de dernier rang) : la valeur du dernier bit de fraction pour cet exposant. Quand un résultat tombe dans un écart, il faut l’arrondir vers l’un des deux voisins. Par défaut, IEEE 754 arrondit au plus proche, à égalité vers le pair : on prend le voisin le plus proche et, si le résultat est exactement au milieu, celui dont le dernier bit vaut 0.
Les égalités sont rares en pratique, mais les entiers juste au-dessus de 2²⁴ les montrent clairement. Un float a 24 bits significatifs, donc entre 2²⁴ = 16 777 216 et 2²⁵ l’écart vaut 2 : seuls les entiers pairs existent. 16 777 217 est exactement à mi-chemin entre 16 777 216 et 16 777 218. L’égalité va au voisin dont le dernier bit est pair, 16 777 216. Mais 16 777 219, à mi-chemin entre 16 777 218 et 16 777 220, est arrondi vers le haut, parce que cette fois c’est 16 777 220 qui a le dernier bit pair :
À essayer : Tapez un nombre ou cliquez sur un exemple, changez de format (en haut à droite), ou cliquez sur un bit pour l’inverser.
- sign
- 0 → positive
- exponent
- 151 − 127 = 24
- significand
- 1.fraction = 1.0000002384185791015625
- value
- +1.0000002384185791015625 × 2^24 = 16777220
Rounding is round-to-nearest, ties-to-even, as in hardware. Sums like 0.1+0.2 are computed in double precision first, as a program would, then stored in the chosen format.
Arrondir vers le pair plutôt que toujours vers le haut évite une dérive systématique quand on arrondit de nombreux résultats. La norme définit aussi trois modes d’arrondi dirigés (vers zéro, vers +∞ et vers −∞), utilisés en arithmétique d’intervalles, où l’on calcule une borne inférieure et une borne supérieure garanties d’encadrer le vrai résultat.
L’epsilon machine
L’écart juste au-dessus de 1,0 s’appelle l’epsilon machine. Pour un float, le nombre qui suit 1 est 1 + 2⁻²³ = 1,00000011920928955078125, donc epsilon vaut 2⁻²³ ≈ 1,19 × 10⁻⁷. Pour un double, c’est 2⁻⁵² ≈ 2,22 × 10⁻¹⁶. Le <float.h> du C les définit comme FLT_EPSILON et DBL_EPSILON ; Python a sys.float_info.epsilon.
Epsilon mesure la précision relative : arrondir correctement un résultat au flottant le plus proche le modifie d’au plus un demi-ulp, soit une erreur relative d’au plus epsilon / 2. C’est de là que viennent les règles empiriques « environ 7 chiffres décimaux significatifs pour un float, environ 16 pour un double ». Cela veut aussi dire qu’ajouter quelque chose de plus petit qu’un demi-ulp ne fait rien du tout : en simple précision, 1 + 2⁻²⁴ est arrondi exactement à 1 (encore une égalité, tranchée vers le pair), et en double précision, 10¹⁶ + 1 − 10¹⁶ vaut 0, parce que l’écart entre doubles près de 10¹⁶ vaut 2.
Les valeurs spéciales
IEEE 754 réserve le plus petit et le plus grand champ d’exposant à des cas particuliers :
| Champ d’exposant | Fraction | Signification |
|---|---|---|
| que des zéros | nulle | ±0 |
| que des zéros | non nulle | nombres sous-normaux : 0,fraction × 2⁻¹²⁶, sans 1 caché |
| que des uns | nulle | ±∞ |
| que des uns | non nulle | NaN (not a number, pas un nombre) |
Les sous-normaux comblent l’écart entre le plus petit float normal, 2⁻¹²⁶ ≈ 1,18 × 10⁻³⁸, et zéro, jusqu’à 2⁻¹⁴⁹ ≈ 1,4 × 10⁻⁴⁵. Ils abandonnent de la précision progressivement au lieu de sauter à zéro. La norme de 1985 les appelait nombres dénormalisés ; la révision de 2008 les a rebaptisés sous-normaux.
Diviser un nombre fini par zéro ne donne l’infini que si ce nombre est non nul : en C, sur la machine où ce chapitre a été écrit, 1.0/0.0 a affiché inf, -1.0/0.0 a affiché -inf, mais 0.0/0.0 a affiché nan. De même pour ∞ − ∞, ∞ / ∞ et la racine carrée d’un nombre négatif.
Le format étendu sur 80 bits ne fait pas partie des formats de base de la norme. La norme de 1985 définissait la simple et la double précision, et laissait les formats « étendus » peu spécifiés ; le format 80 bits est l’implémentation de l’un d’eux par l’unité x87. La révision de 2008 a ajouté les formats binaires de 16 et 128 bits, la virgule flottante décimale, et la multiplication-addition fusionnée (fused multiply-add : a × b + c avec un seul arrondi), que tous les CPU modernes ont désormais.
Pourquoi comparer des flottants avec == est risqué
Comme presque chaque opération arrondit, deux calculs égaux en mathématiques diffèrent souvent sur leur dernier bit :
0.1 + 0.2 == 0.3 → false
(0.1 + 0.2) + 0.3 → 0.6000000000000001
0.1 + (0.2 + 0.3) → 0.6
L’addition flottante n’est pas associative : l’ordre des opérations change le résultat. Un compilateur ne peut donc pas réordonner des additions flottantes sans votre permission (avec des options comme -ffast-math), et une somme parallèle peut donner des réponses légèrement différentes selon le découpage du travail.
La parade habituelle est de comparer avec une tolérance, relative à la taille des nombres : |a − b| ≤ rel × max(|a|, |b|), plus une petite tolérance absolue pour les valeurs proches de zéro. math.isclose(0.1 + 0.2, 0.3) en Python fait exactement cela, et renvoie True. Deux autres pièges : un NaN est différent de tout, y compris de lui-même (x != x est le test classique de NaN), et −0 == +0 est vrai bien que les bits diffèrent.
La comparaison exacte convient quand les valeurs sont exactes : les petits entiers, et les sommes de puissances de deux comme 5,75, sont représentés et calculés exactement.
Sommer beaucoup de nombres : l’astuce de Kahan
Additionner une longue liste de nombres perd de la précision à chaque étape, et les erreurs s’accumulent. Le cas extrême : ajouter 1,0 à un float 20 millions de fois. Une fois que le total atteint 16 777 216, ajouter 1 donne une égalité arrondie vers le bas, et la somme cesse de croître. Mesurée avec clang sur le M2, la boucle naïve s’arrête à 16 777 216.
La sommation de Kahan (sommation compensée), due à William Kahan, principal auteur d’IEEE 754, garde une seconde variable contenant l’erreur d’arrondi de la dernière addition et la réinjecte dans la suivante :
float sum = 0.0f, c = 0.0f; /* c = the part that got lost */
for (int i = 0; i < n; i++) {
float y = x[i] - c; /* add back what was lost last time */
float t = sum + y; /* big + small: low bits of y are lost */
c = (t - sum) - y; /* (t - sum) is what was actually added */
sum = t;
}
Avec les mêmes 20 millions de uns, toujours entièrement en float, elle renvoie exactement 20 000 000. Ajouter 0.1f dix millions de fois donne 1 087 937 naïvement et 1 000 000 avec la méthode de Kahan. Compilez-la sans -ffast-math : cette option autorise le compilateur à traiter l’arithmétique flottante comme associative, à simplifier (t - sum) - y en zéro et à réordonner la boucle. Compilées avec, les deux boucles sur 0.1f ont renvoyé le même nombre, 1 005 958,5625 : la compensation avait disparu, et la réponse exacte avec elle.
Python applique la même idée pour vous : depuis Python 3.12, la fonction sum() intégrée utilise un algorithme compensé, si bien que sum([0.1] * 10) vaut 1.0, alors qu’une simple boucle s += 0.1 s’arrête à 0.9999999999999999. Pour une somme arrondie exactement, math.fsum va plus loin.
À retenir
- La virgule flottante stocke une mantisse et un exposant, ce qui sépare la précision de l’étendue, avec une précision relative à peu près constante.
- IEEE 754 binary32 : 1 bit de signe, 8 bits d’exposant en excédent 127, 23 bits de fraction après un 1 caché. binary64 : 1, 11 (excédent 1023), 52.
- À la main : passer en binaire, normaliser en 1,f × 2^e, stocker e + biais et f. 5,75 =
0x40B80000, −0,15625 =0xBE200000. - Les résultats sont arrondis au plus proche, à égalité vers le pair : 16 777 217 devient 16 777 216, mais 16 777 219 devient 16 777 220.
- L’epsilon machine vaut 2⁻²³ ≈ 1,19 × 10⁻⁷ pour un
float, 2⁻⁵² ≈ 2,22 × 10⁻¹⁶ pour undouble: environ 7 et 16 chiffres décimaux. - Valeurs spéciales : ±0, sous-normaux (autrefois appelés « dénormalisés »), ±∞, et NaN, que produit 0/0, et non ∞.
- L’arithmétique flottante n’est pas associative : comparez avec une tolérance, jamais avec
==sur des résultats calculés ; la sommation de Kahan récupère les chiffres qu’une longue somme perd.