IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Nombres en machine : précision, limites et pièges

Pourquoi 0.1 + 0.2 ≠ 0.3 ?

Objectif : comprendre pourquoi les nombres manipulés en machine ne sont que des approximations des nombres mathématiques, et quelles en sont les conséquences.

Niveau requis : bases en programmation et notions élémentaires de numération (bases 2 et 10).

Commentez cet article : 3 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

En mathématiques, l’égalité suivante est triviale :

kitxmlcodelatexdvp{0.1 + 0.2 = 0.3}finkitxmlcodelatexdvp

En machine, cette même opération donne pourtant un résultat inattendu :

 
Sélectionnez
>>> 0.1 + 0.2
>>> 0.30000000000000004

Ce 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}}finkitxmlcodelatexdvp

En 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}}finkitxmlcodelatexdvp

Cette é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}}finkitxmlcodelatexdvp

En pratique, on n’écrit généralement que les termes non nuls :

kitxmlcodelatexdvp{5.25_{10} = 2^2 + 2^0 + 2^{-2}}finkitxmlcodelatexdvp

Chaque 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.

 
Sélectionnez
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}}finkitxmlcodelatexdvp

Ce 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}finkitxmlcodelatexdvp

De manière analogue, les ordinateurs utilisent une notation scientifique en base 2 :

kitxmlcodelatexdvp{x = (-1)^s \times m \times 2^e}finkitxmlcodelatexdvp

Cette 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}finkitxmlcodelatexdvp

Dans 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}}finkitxmlcodelatexdvp

III-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}finkitxmlcodelatexdvp

III-C-4. Exemple complet

Considérons le nombre suivant, écrit en notation scientifique binaire :

kitxmlcodelatexdvp{1.01_2 \times 2^2}finkitxmlcodelatexdvp

On obtient :

kitxmlcodelatexdvp{1.01_{2} = 1 \times 2^0 + 0 \times 2^{-1} + 1 \times 2^{-2} = 1.25}finkitxmlcodelatexdvp

Donc :

kitxmlcodelatexdvp{1.25 \times 2^2 = 5}finkitxmlcodelatexdvp

Ce 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.

Structure IEEE754

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 :

 
Sélectionnez
0.0001100110011001100110011...

On peut normaliser cette écriture sous la forme :

kitxmlcodelatexdvp{0.1_{10} = 1.1001100110011..._2 \times 2^{-4}}finkitxmlcodelatexdvp

Dans 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 :

 
Sélectionnez
0.1000000000000000055511151231257827021181583404541015625

La 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 :

 
Sélectionnez
0.1 + 0.2

On obtient :

 
Sélectionnez
0.30000000000000004

Ce 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é :

 
Sélectionnez
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 :

 
Sélectionnez
a = 1.0000001
b = 1.0000000

result = a - b
print(result)

Ce qui donne :

 
Sélectionnez
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é.

 
Sélectionnez
a = 1e16
b = 1

print(a + b)

Résultat :

 
Sélectionnez
10000000000000000.0

Dans 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électionnez
s = 0.0
for i in range(1000000):
    s += 0.1

print(s)

Qui nous donne :

 
Sélectionnez
100000.00000133288

Le 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.

 
Sélectionnez
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 total

Cet 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 == :

 
Sélectionnez
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) :

 
Sélectionnez
abs(total - 0.3) < 1e-9

Cependant, 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) :

 
Sélectionnez
import math
math.isclose(0.1 + 0.2, 0.3)  # Renvoie True

Elle repose sur la relation suivante :

kitxmlcodelatexdvp{|a - b| \le \max(\text{rel_tol} \times \max(|a|, |b|), \text{abs_tol})}finkitxmlcodelatexdvp

En 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 :

 
Sélectionnez
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 exactement

Contrairement 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() :

 
Sélectionnez
# 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

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Licence Creative Commons
Le contenu de cet article est rédigé par Denis Hulo et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2026 Developpez.com.