I. Introduction▲
En mathématiques, l’égalité suivante est triviale :
kitxmlcodelatexdvp{0.1 + 0.2 = 0.3}finkitxmlcodelatexdvpEn machine, cette même opération donne pourtant un résultat inattendu :
>>> 0.1 + 0.2
>>> 0.30000000000000004Ce comportement n’est pas une erreur du langage, mais une conséquence directe de la manière dont les nombres sont représentés en machine.
Cet article propose d’explorer les différences fondamentales entre la représentation des nombres en mathématiques et en machine, afin de comprendre pourquoi ces écarts existent et comment les maîtriser.
Tous les exemples de code présentés dans cet article sont écrits en Python, mais les concepts abordés sont valables dans la plupart des langages de programmation.
Pour mieux comprendre ces différences, commençons par rappeler comment les nombres sont définis en mathématiques.
II. Représentation mathématique des nombres▲
En mathématiques, les nombres sont des objets abstraits définis indépendamment de toute contrainte matérielle.
Ils existent en tant qu’entités idéales : leur représentation (écriture décimale, fractionnaire ou symbolique) n’est qu’un moyen de les décrire, et non le nombre lui-même.
Un même nombre peut avoir plusieurs représentations : 1/3, 0.333…, ou encore, être défini comme la limite d’une suite de valeurs qui s’en approchent progressivement.
Cette distinction est essentielle : en mathématiques, un nombre n’est pas uniquement défini par son écriture décimale. Il peut être décrit de manière exacte, même si son écriture complète comporte une infinité de décimales.
Les nombres réels forment un ensemble continu : entre deux nombres, il en existe toujours une infinité d’autres.
Cette continuité s’oppose directement à la représentation en machine, qui repose sur un ensemble fini de valeurs.
Voyons maintenant comment ces nombres sont représentés concrètement en informatique.
III. Représentation des nombres en machine▲
III-A. Base 2 vs base 10▲
Les ordinateurs utilisent le système binaire (base 2), contrairement à notre système décimal (base 10), qui est celui utilisé en mathématiques et dans la vie courante.
Dans un système de numération, les nombres sont exprimés comme des sommes de puissances de la base. Chaque chiffre indique si une puissance de la base est présente ou non.
Par exemple, en base 10 :
kitxmlcodelatexdvp{5.25_{10} = 5 \times 10^0 + 2 \times 10^{-1} + 5 \times 10^{-2}}finkitxmlcodelatexdvpEn base 2, les nombres sont exprimés comme des sommes de puissances de 2 (22, 21, 20, 2-1, etc.).
Le même nombre en base 2 s’écrit alors :
kitxmlcodelatexdvp{5.25_{10} = 101.01_{2}}finkitxmlcodelatexdvpCette écriture correspond à la décomposition suivante :
kitxmlcodelatexdvp{101.01_{2} = 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 + 0 \times 2^{-1} + 1 \times 2^{-2}}finkitxmlcodelatexdvpEn pratique, on n’écrit généralement que les termes non nuls :
kitxmlcodelatexdvp{5.25_{10} = 2^2 + 2^0 + 2^{-2}}finkitxmlcodelatexdvpChaque chiffre, en base 2, indique la présence (1) ou l’absence (0) d’une puissance de 2.
Cette différence de base a des conséquences importantes, notamment sur la représentation des fractions.
III-B. Représentation des fractions en base 2▲
Toutes les fractions ne peuvent pas être écrites de manière finie en base 2.
Une fraction possède une écriture finie en base 2 si, et seulement si, son dénominateur (une fois la fraction réduite) est une puissance de 2.
Par exemple :
- kitxmlcodeinlinelatexdvp{\frac{1}{2} = 0.1_{2} = 1 \times 2^{-1}}finkitxmlcodeinlinelatexdvp
- kitxmlcodeinlinelatexdvp{\frac{1}{4} = 0.01_{2} = 1 \times 2^{-2}}finkitxmlcodeinlinelatexdvp
- kitxmlcodeinlinelatexdvp{\frac{1}{8} = 0.001_{2} = 1 \times 2^{-3}}finkitxmlcodeinlinelatexdvp
En revanche, certaines fractions comme 1/10 ne vérifient pas cette propriété et ne peuvent pas être représentées de manière finie en base 2.
Pour comprendre pourquoi, on peut convertir 0.1 en base 2 en multipliant successivement par 2. À chaque étape, la partie entière du résultat forme un bit, tandis que la partie fractionnaire est conservée pour l’itération suivante.
0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1
0.2 × 2 = 0.4 → 0
...Cela s’explique par le fait que le nombre peut s’écrire sous la forme :
kitxmlcodelatexdvp{0.1 = b_1 \cdot 2^{-1} + b_2 \cdot 2^{-2} + b_3 \cdot 2^{-3} + \dots}finkitxmlcodelatexdvpÀ chaque multiplication par 2, un nouveau bit (b₁, b₂, etc.) est extrait : il correspond à la partie entière obtenue à chaque étape.
On observe que le processus entre dans une boucle : la représentation binaire est donc infinie et périodique.
kitxmlcodelatexdvp{0.1_{10} = 0.0001100110011..._{2}}finkitxmlcodelatexdvpCe phénomène est similaire à celui des fractions en base 10, comme 1/3 qui s’écrit 0.333…
Une infinité de nombres à l’écriture décimale simple deviennent des écritures infinies en base 2, ce qui impose une approximation en machine.
III-C. Flottants et notation scientifique▲
Les nombres réels sont représentés en machine à l’aide de nombres à virgule flottante. Cette écriture s’inspire directement de la notation scientifique utilisée en mathématiques.
En base 10, un nombre comme 1234,5 peut s’écrire :
kitxmlcodelatexdvp{1234.5 = 1.2345 \times 10^3}finkitxmlcodelatexdvpDe manière analogue, les ordinateurs utilisent une notation scientifique en base 2 :
kitxmlcodelatexdvp{x = (-1)^s \times m \times 2^e}finkitxmlcodelatexdvpCette formule se décompose en trois éléments fondamentaux :
- s (signe) : détermine si le nombre est positif (0) ou négatif (1).
- m (mantisse) : contient les chiffres significatifs du nombre.
- e (exposant) : indique la puissance de 2 par laquelle la mantisse est multipliée.
III-C-1. Le bit de signe▲
Le signe est représenté par un seul bit :
- 0 : nombre positif
- 1 : nombre négatif
Le terme (-1)s permet ainsi de gérer simplement les nombres positifs et négatifs.
III-C-2. La mantisse▲
La mantisse (ou significande) contient les chiffres significatifs du nombre.
En pratique, les nombres flottants sont normalisés : la mantisse est toujours comprise entre 1 et 2.
kitxmlcodelatexdvp{1 \le m < 2}finkitxmlcodelatexdvpDans la norme IEEE 754, que nous détaillerons plus loin, le premier bit de la mantisse est implicite (toujours égal à 1), ce qui permet de gagner en précision.
Par exemple, en base 2 :
kitxmlcodelatexdvp{1.101_2 = 1 \times 2^0 + 1 \times 2^{-1} + 0 \times 2^{-2} + 1 \times 2^{-3}}finkitxmlcodelatexdvpIII-C-3. L’exposant▲
L’exposant indique combien de fois la mantisse doit être multipliée (ou divisée) par 2.
Il permet de représenter des nombres très grands ou très petits.
- Exposant positif → le nombre est multiplié par une puissance de 2
- Exposant négatif → le nombre est divisé par une puissance de 2
Par exemple :
kitxmlcodelatexdvp{1.5 \times 2^3 = 12}finkitxmlcodelatexdvpIII-C-4. Exemple complet▲
Considérons le nombre suivant, écrit en notation scientifique binaire :
kitxmlcodelatexdvp{1.01_2 \times 2^2}finkitxmlcodelatexdvpOn obtient :
kitxmlcodelatexdvp{1.01_{2} = 1 \times 2^0 + 0 \times 2^{-1} + 1 \times 2^{-2} = 1.25}finkitxmlcodelatexdvpDonc :
kitxmlcodelatexdvp{1.25 \times 2^2 = 5}finkitxmlcodelatexdvpCe mécanisme est identique à la notation scientifique en base 10, mais avec des puissances de 2.
Cette écriture efficace a toutefois une conséquence importante : tous les nombres ne peuvent pas être représentés exactement, ce qui introduit des erreurs d’arrondi.
Cette représentation générale est formalisée par une norme largement utilisée en informatique.
IV. Norme IEEE 754 (niveau avancé)▲
IV-A. Structure d’un flottant▲
La norme IEEE 754 définit la représentation des nombres flottants utilisée par la quasi-totalité des ordinateurs modernes.
En simple précision (32 bits), un nombre est décomposé ainsi :
- 1 bit de signe ;
- 8 bits d’exposant ;
- 23 bits de mantisse.
L’exposant est stocké sous forme d’un entier non signé. Un biais est ajouté à sa valeur réelle afin de permettre le codage d’exposants négatifs sans recourir à une représentation d'entier signé (telle que le complément à deux).
La mantisse est normalisée : les nombres sont écrits sous la forme 1.xxxxx × 2e. Le premier bit, toujours égal à 1, est implicite et n’est pas stocké, ce qui permet d’augmenter la précision.
Cette structure permet de couvrir un très large intervalle de valeurs, mais avec une précision limitée.
Le format simple précision (32 bits) est présenté ici pour faciliter la compréhension. En pratique, la plupart des langages utilisent la double précision (64 bits), qui repose sur les mêmes principes, mais avec une mantisse et un exposant plus grands, offrant ainsi une meilleure précision.
Avant de passer à l'exemple, n'hésitez pas à tester vos propres valeurs sur ce visualiseur IEEE 754visualiseur IEEE 754.
Illustrons maintenant ce format avec un exemple concret.
IV-B. Exemple concret▲
Considérons le nombre 0,1 en base 10. Comme évoqué précédemment, il ne peut pas être écrit exactement en base 2.
Sa représentation binaire est donc infinie et périodique :
0.0001100110011001100110011...On peut normaliser cette écriture sous la forme :
kitxmlcodelatexdvp{0.1_{10} = 1.1001100110011..._2 \times 2^{-4}}finkitxmlcodelatexdvpDans la norme IEEE 754, ce nombre est alors codé en trois parties :
- Signe : 0 (nombre positif) ;
- Mantisse : 100110011… (le bit initial 1 étant implicite) ;
- Exposant : -4, stocké avec un biais (−4 + 127 = 123 en simple précision).
Cependant, la mantisse étant limitée à un nombre fini de bits, cette écriture doit être approximée lors de sa mise en mémoire.
La plupart des langages, comme Python, utilisent en réalité la double précision (64 bits). Dans ce format, la valeur effectivement manipulée est :
0.1000000000000000055511151231257827021181583404541015625La valeur stockée n’est pas exactement 0,1, mais une approximation très proche.
Ce résultat provient directement des limites des nombres flottants.
Cet écart, bien que minime, est à l’origine des erreurs observées lors des calculs, que nous allons illustrer dans la section suivante.
IV-C. Erreurs d’arrondi▲
Les limitations des nombres flottants ont des conséquences directes sur les calculs.
Les valeurs manipulées étant souvent des approximations, chaque opération peut introduire un nouvel arrondi. Ces écarts peuvent alors se combiner au fil des calculs.
Considérons l’exemple suivant :
0.1 + 0.2On obtient :
0.30000000000000004Ce résultat ne provient pas d’une erreur de calcul, mais des limites de la représentation des nombres flottants. L’addition est correctement effectuée, mais elle porte sur des valeurs déjà approchées, et son résultat est lui-même arrondi pour s'adapter au format flottant.
Les erreurs d’arrondi sont inhérentes à ce mode d’écriture : elles apparaissent dès qu’une valeur ne peut pas être exprimée exactement avec un nombre fini de bits.
L’accumulation des erreurs au fil des opérations peut amplifier ces écarts et produire des résultats significativement différents de ceux attendus en mathématiques.
Il est donc essentiel de connaître ces limitations pour concevoir des programmes fiables.
V. Conséquences pratiques▲
V-A. Bugs subtils et comparaisons▲
L’une des conséquences les plus concrètes de l’imprécision des flottants binaires est l’apparition d’erreurs lors des comparaisons, en particulier lors des tests d’égalité :
total = 0.1 + 0.2
# En réalité, total vaut 0.30000000000000004
if total == 0.3:
print("OK")
else:
print("Erreur")Ce code affiche "Erreur". En effet, les valeurs manipulées sont des approximations, et la comparaison directe avec l’opérateur == est donc risquée.
Comme nous le verrons plus loin, en pratique, on utilise une tolérance adaptée au contexte, ou des fonctions comme math.isclose() pour effectuer des comparaisons fiables.
V-B. Accumulation des erreurs▲
Lorsque de nombreuses opérations sont effectuées, les erreurs d’arrondi peuvent s’accumuler.
Cela peut conduire à des résultats numériquement instables, notamment dans les calculs scientifiques.
Au-delà des erreurs d’arrondi classiques, certains phénomènes numériques plus subtils peuvent apparaître dans les calculs.
V-C. Domaines critiques▲
Dans certains domaines, ces erreurs ne sont pas anodines :
- Finance : erreurs d’arrondi cumulées sur de grandes quantités de transactions
- Simulation scientifique : propagation des erreurs dans les modèles numériques
- Graphisme 3D : artefacts visuels liés aux imprécisions.
Une mauvaise gestion des flottants peut entraîner des erreurs coûteuses ou critiques.
Les problèmes présentés jusqu’ici illustrent des situations courantes rencontrées en pratique. Ils trouvent leur origine dans des phénomènes numériques plus profonds, que nous allons maintenant examiner en détail.
VI. Erreurs numériques avancées▲
VI-A. Annulation numérique (cancellation catastrophique)▲
L'annulation numérique se produit lorsque l’on soustrait deux nombres très proches.
Considérons l’exemple suivant :
a = 1.0000001
b = 1.0000000
result = a - b
print(result)Ce qui donne :
1.0000000005838672e-07À première vue, on s’attend à obtenir exactement 10-7. Cependant, comme nous l’avons déjà dit, en arithmétique flottante, les nombres manipulés sont déjà des approximations. Lorsque deux valeurs très proches sont soustraites, leurs chiffres significatifs s’annulent, et le résultat repose principalement sur les bits les moins précis. Cela peut entraîner une perte de précision, généralement faible, comme dans cet exemple, mais pouvant devenir significative dans d’autres situations : on parle alors de cancellation catastrophique.
Ce phénomène est particulièrement problématique dans les algorithmes itératifs ou les calculs scientifiques.
VI-B. Absorption numérique▲
Les nombres flottants ont une précision limitée. Lorsqu’un nombre très petit est ajouté à un nombre très grand, il peut être complètement ignoré.
a = 1e16
b = 1
print(a + b)Résultat :
10000000000000000.0Dans la norme IEEE 754, l’addition nécessite d’aligner les exposants. Ici, le nombre 1 est tellement petit devant 1e16 que le décalage de sa mantisse dépasse la précision disponible : ses bits significatifs sont perdus et sa contribution devient nulle.
Les petits termes peuvent disparaître lorsqu’ils sont combinés avec des valeurs très grandes.
VI-C. Sommes répétées et erreur cumulée▲
L’addition répétée de petits nombres peut produire un résultat inattendu.
s = 0.0
for i in range(1000000):
s += 0.1
print(s)Qui nous donne :
100000.00000133288Le résultat peut différer sensiblement de la valeur attendue.
Cette erreur provient de l’accumulation des approximations successives.
VI-D. Technique d’amélioration : somme de Kahan▲
Il existe des techniques pour limiter ces erreurs, comme l’algorithme de Kahan.
def kahan_sum(values):
total = 0.0
c = 0.0
for v in values:
y = v - c
t = total + y
c = (t - total) - y
total = t
return totalCet algorithme compense les erreurs d’arrondi accumulées.
Kahan est utilisé dans des contextes où la précision est critique (calcul scientifique, finance).
Cet algorithme montre qu’il est possible de concevoir des méthodes numériques plus robustes pour limiter l’accumulation des erreurs.
Au-delà de ces techniques spécifiques, il est essentiel d’adopter de bonnes pratiques pour limiter ces effets dans les programmes courants.
VII. Bonnes pratiques▲
VII-A. Éviter les comparaisons directes▲
Comme illustré précédemment (section V-ABugs subtils et comparaisons), il ne faut pas comparer directement deux nombres flottants avec l’opérateur == :
total = 0.1 + 0.2
# En réalité, total vaut 0.30000000000000004
if total == 0.3:
print("OK")Une approche courante consiste à vérifier si la différence entre deux valeurs est inférieure à un petit seuil (souvent appelé epsilon) :
abs(total - 0.3) < 1e-9Cependant, un seuil fixe n’est pas toujours adapté : une tolérance de 10-9 est négligeable pour comparer des millions, mais énorme pour comparer des milliardièmes.
Pour une solution plus robuste, le module math propose la fonction math.isclose(), qui ajuste automatiquement la tolérance en fonction de l’ordre de grandeur des nombres (tolérance relative) :
import math
math.isclose(0.1 + 0.2, 0.3) # Renvoie TrueElle repose sur la relation suivante :
kitxmlcodelatexdvp{|a - b| \le \max(\text{rel_tol} \times \max(|a|, |b|), \text{abs_tol})}finkitxmlcodelatexdvpEn combinant une tolérance relative (rel_tol) et une tolérance absolue (abs_tol) pour les valeurs proches de zéro, math.isclose() permet d’obtenir des comparaisons fiables quelle que soit l’échelle des données.
VII-B. Utiliser des types adaptés▲
Dans certains contextes, notamment en finance ou pour des calculs nécessitant une grande précision, les nombres flottants binaires ne sont pas adaptés.
Il est alors préférable d’utiliser des types décimaux, comme avec le module decimal en Python :
from decimal import Decimal
# On utilise des chaînes pour éviter l'imprécision des floats
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b) # Affiche 0.3 exactementContrairement aux flottants standards, la bibliothèque decimal représente les nombres en base 10 sous la forme d’un coefficient et d’un exposant : kitxmlcodeinlinelatexdvp{\text{valeur} = \text{coefficient} \times 10^{\text{exposant}}}finkitxmlcodeinlinelatexdvp
Par exemple, 0.1 correspond à 1 × 10-1 et 0.2 à 2 × 10-1.
Cette structure interne est accessible via la méthode as_tuple() :
# Pour 0.1 : coefficient = 1, exposant = -1
Decimal("0.1").as_tuple()
# DecimalTuple(sign=0, digits=(1,), exponent=-1)Lors d’une addition, si les exposants sont identiques, les coefficients peuvent être additionnés directement : kitxmlcodeinlinelatexdvp{(1 + 2) \times 10^{-1} = 0.3}finkitxmlcodeinlinelatexdvp. Cela évite les erreurs d’arrondi liées à la représentation binaire des flottants.
Cependant, la précision reste limitée par le contexte (28 chiffres significatifs par défaut). De plus, les calculs avec Decimal sont plus coûteux en ressources que ceux utilisant des flottants natifs. Enfin, certaines opérations (comme les divisions) peuvent toujours produire des résultats arrondis.
VII-C. Comprendre la précision des types▲
Tous les types numériques n’offrent pas la même précision.
Par exemple, un flottant double précision (64 bits) offre environ 15 à 17 chiffres significatifs.
Au-delà de cette limite, la précision des résultats n’est plus garantie.
VII-D. Limiter les erreurs cumulées▲
Évitez d’accumuler des opérations sensibles, notamment dans des boucles.
Privilégiez des algorithmes numériquement stables lorsque c’est possible.
VIII. Conclusion▲
Les mathématiques et la représentation des nombres en machine reposent sur deux visions différentes : l’une idéale, continue et infinie ; l’autre concrète, discrète et limitée par les capacités des ordinateurs.
La seconde ne reproduit pas fidèlement la première, mais en propose une approximation nécessaire pour rendre les calculs exploitables en pratique.
Comprendre cette différence est essentiel pour éviter les pièges du calcul numérique et concevoir des programmes fiables.
Un développeur ne manipule pas seulement des nombres : il doit aussi comprendre les limites de leur représentation.
Cette prise de conscience marque souvent le passage d’une programmation intuitive à une programmation plus rigoureuse.
IX. Remerciements▲
Je tiens à remercier tout particulièrement f-leb pour son aide précieuse dans la réalisation de cet article, ainsi que Laurent Ott pour ses commentaires encourageants sur les premières versions de ce travail.
Sources et références





