La limite du silicium : pourquoi les calculs en virgule flottante et entiers échouent silencieusement

La limite du silicium : pourquoi les calculs en virgule flottante et entiers échouent silencieusement

Table des matières

J’ai récemment développé une interface de métriques d’exécution pour suivre les performances d’une application. J’ai choisi le type float pour stocker les valeurs, car il semblait suffisant pour représenter des nombres décimaux.

Lorsque nous avons vérifié les données, les résultats étaient faux. Ils ne correspondaient pas aux résultats attendus. Nous avons supposé que la logique de calcul était défectueuse et avons passé des heures à déboguer le code de calcul.

Les résultats étaient également incohérents. Parfois les valeurs étaient correctes. D’autres fois, elles dérivaient progressivement de la réalité.

Nous avons finalement trouvé la cause : la précision limitée du type float. Il ne disposait pas d’une précision suffisante pour notre plage de valeurs. La correction a été simple : nous sommes passés à double, et les erreurs ont disparu.

Choisir le mauvais type de données est un tueur silencieux en logiciel. Cela ne fait pas toujours planter votre programme. À la place, cela corrompt lentement vos données.

Ces problèmes apparaissent souvent sous forme d’erreurs de précision en virgule flottante ou de dépassement d’entier, qui peuvent tous deux altérer silencieusement les calculs.

Pourquoi les petits choix comptent : Ariane et Boeing

Ce n’est pas simplement un petit bug. L’histoire montre que les erreurs numériques peuvent être catastrophiques.

  • La fusée Ariane 5 (1996) : Un nombre en virgule flottante sur 64 bits a été converti en entier signé sur 16 bits. La valeur était trop grande. Cela a provoqué un dépassement. La fusée s’est auto-détruite 37 secondes après le décollage. Lire le rapport complet ici.
  • Le bug électrique du Boeing 787 : Un compteur logiciel utilisait un entier signé sur 32 bits pour mesurer le temps en centisecondes. Si l’avion restait alimenté pendant 248 jours, le compteur débordait. Cela pouvait entraîner une coupure totale de l’alimentation électrique en plein vol. Voir la directive de la FAA.

Cas 1 : Les entiers et leur limite stricte

Les entiers sont stockés sur un nombre fixe de bits. Un signed int en C utilise généralement 32 bits. Il possède une valeur minimale et maximale fixes.

Représentation : Les entiers utilisent un format binaire direct (souvent le complément à deux pour les valeurs signées). Il n’y a pas de fraction.

Représentation d’un entier

Dépassement (overflow / underflow) : Lorsqu’un entier atteint sa limite, il “reboucle”.

Rebouclage d’entier

Exemple :

1#include <stdio.h>
2
3int main() {
4    unsigned char max_val = 255; 
5    printf("Max: %hhu\n", max_val);
6    printf("Overflow: %hhu\n", max_val + 1); // Devient 0
7    return 0;
8}

La sortie est :

Max: 255
Overflow: 0

Nous utilisons ici un entier non signé sur 8 bits (unsigned char en C), la valeur maximale est 255 (2^8 - 1).

  • Le calcul : 255 + 1
255=(1111 1111)b et 1=(0000 0001)b \begin{aligned} 255 = (1111\ 1111)_b\text{ et }1 = (0000\ 0001)_b \end{aligned} 1111 1111+ 0000 00011 0000 0000 \begin{aligned} &1111\ 1111 \\ +\ &0000\ 0001 \\ \hline \color{red}{1}\ &\color{green}{0000\ 0000} \end{aligned}

Mais attention : le registre ne contient que 8 bits. Ce 1 en tête n’a nulle part où aller. Il est reporté dans un drapeau (flag) du processeur, et le registre conserve 00000000.

Un comportement similaire apparaît lors d’une soustraction, appelé underflow. Par exemple, si vous soustrayez 1 à 0, vous obtenez 255 avec un entier non signé sur 8 bits.

Pourquoi les entiers signés sont plus dangereux

Avec les entiers signés, le bit le plus à gauche est le bit de signe (0 pour positif, 1 pour négatif). Si vous dépassez la limite d’un nombre positif, vous basculez ce bit. Un grand montant positif peut alors devenir une dette massive.

1#include <stdio.h>
2
3int main() {
4    signed char max_val = 127; 
5    printf("Max: %hhd\n", max_val);
6    printf("Overflow: %hhd\n", max_val + 2); // Devient -127
7    return 0;
8}

La sortie est :

Max: 127
Overflow: -127
  • Le calcul : 127 + 2
127=(0111 1111)b et 2=(0000 0010)b \begin{aligned} 127 = (0111\ 1111)_b\text{ et }2 = (0000\ 0010)_b \end{aligned} 0111 1111+ 0000 00101000 0001 \begin{aligned} &0111\ 1111 \\ +\ &0000\ 0010 \\ \hline &\color{red}{1}\color{green}{000\ 0001} \end{aligned}

(1000 0001)b(1000\ 0001)_b en complément à deux correspond à -127.

Les entiers rebouclent lorsqu’ils dépassent leurs limites.
Gardez cela en tête lors de calculs arithmétiques.
Vérifiez toujours vos bornes et utilisez des types plus larges si nécessaire.

Cas 2 : Les flottants et la cible mouvante

Les nombres en virgule flottante sont plus complexes.

Représentation : Un float est composé de trois parties :

  1. Bit de signe : positif ou négatif.
  2. Exposant : définit la plage de valeurs (l’échelle).
  3. Mantisse (ou significande) : définit la précision (les chiffres).
Représentation d’un float

Ils suivent généralement la norme IEEE 754. Il existe aussi des formats plus petits, comme les minifloats, utilisés en machine learning et en infographie pour des raisons d’efficacité.

Les deux types de problèmes :

  1. Dépassement vers l’infini : le nombre devient trop grand pour l’exposant et se transforme en inf (infini).
  2. Perte de précision : le nombre reste dans la plage, mais la mantisse est trop petite pour représenter les variations. C’est ce qui s’est produit dans mon interface de métriques.
 1#include <stdio.h>
 2#include <float.h>
 3
 4int main() {
 5    // 1. Dépassement de l’exposant
 6    float big = FLT_MAX; 
 7    printf("Max Float: %e\n", big);
 8    printf("Overflow to Inf: %e\n", big * 2.0f);
 9
10    // 2. "Overflow" de précision (erreur silencieuse)
11    float x = 16777216.0f; // 2^24
12    printf("Original: %f\n", x);
13    printf("Add 1.0:  %f\n", x + 1.0f); // Affiche toujours 16777216.000000
14
15    printf("Max Float and precision: %e\n", big + 1000.0f);
16
17    return 0;
18}

La sortie est :

Max Float: 3.402823e+38
Overflow to Inf: inf
Original: 16777216.000000
Add 1.0:  16777216.000000
Max Float and precision: 3.402823e+38

Dans le second cas, la valeur est tellement grande que l’ajout de 1.0 est perdu, car la mantisse ne peut pas représenter ce niveau de détail.

L’écart entre les nombres représentables augmente de façon exponentielle avec l’exposant.

Fait important : la perte de précision et le dépassement vers l’infini ne sont pas exclusifs. Ajouter un petit nombre à FLT_MAX entraîne généralement une perte de précision ; la valeur peut rester FLT_MAX tant que l’ajout n’est pas assez grand pour provoquer un dépassement vers l’infini.

Distribution des nombres représentables

Pour illustrer cela, visualisons la distribution des nombres pour un float sur 8 bits.

Considérons les formats S1E4M3 (4 bits d’exposant, 3 bits de mantisse) et S1E3M4 (3 bits d’exposant, 4 bits de mantisse).

CaractéristiqueFloat S1E4M3Float S1E3M4
Bits d’exposant43
Bits de mantisse34
Nombres distincts239223
Valeur min-240.0-15.5
Valeur max240.015.5
Point fortLarge plageHaute précision

En excluant -inf, inf et NaN, S1E4M3 peut représenter 239 nombres distincts entre -240.0 et 240.0, tandis que S1E3M4 peut en représenter 223 entre -15.5 et 15.5.

Cependant, S1E3M4 offre plus de précision pour les petites valeurs grâce à sa mantisse plus large, tandis que S1E4M3 couvre une plage plus large mais avec moins de précision.

Visualisons maintenant la distribution pour S1E4M3.

Distribution des nombres représentables pour un float 8 bits avec 4 bits d’exposant et 3 bits de mantisse - pas de 0.001.
Pas de 0.001 unité

Aucune valeur n’existe entre les pics. Plus on s’éloigne de zéro, plus les valeurs deviennent espacées.

À partir de la valeur 16.0, nous ne pouvons plus représenter tous les entiers car l’écart entre deux valeurs représentables devient supérieur à 1. Ce seuil vient de :

2bits_de_mantisse+1=23+1=24 2^{bits\_de\_mantisse + 1} = 2^{3+1} = 2^4

Par exemple, 17.0 ne peut pas être représenté. C’est exactement le problème rencontré dans mon interface de métriques.

Distribution des nombres représentables pour un float 8 bits avec 4 bits d’exposant et 3 bits de mantisse - pas de 1.0.
Autre vue (pas de 1.0)

Gérer le “rien”

Les floats possèdent un état spécial appelé NaN (Not a Number). Il apparaît lors d’opérations indéfinies, comme 00\frac{0}{0} ou la racine carrée d’un nombre négatif. Vous pouvez en apprendre plus sur NaN sur Wikipedia.

Les entiers gèrent ces situations différemment. Ils ne disposent pas d’un état “Not a Number” (NaN). Si vous effectuez une opération invalide comme une division par zéro, le comportement est indéfini et peut provoquer un crash du programme. Ils doivent toujours représenter un motif binaire valide dans leur plage.

Conclusion

Vous devez comprendre les besoins de vos types numériques : la plage de valeurs à représenter ainsi que la perte de précision acceptable.

  • Utilisez des entiers lorsque vous avez besoin de valeurs exactes et que vous pouvez prévoir la plage maximale.
  • Utilisez double (64 bits) par défaut pour les nombres décimaux. N’utilisez float (32 bits) ou les minifloats que si vous avez des contraintes mémoire fortes et que vous avez vérifié que la perte de précision est acceptable.
  • Lors des opérations arithmétiques, testez toujours les cas limites, en particulier aux frontières des types.

Si vous utilisez des nombres en virgule flottante, faites attention à la diminution exponentielle de la précision lorsque les valeurs augmentent.

Vous pouvez approfondir la distribution et la précision des floats dans l’explication visuelle de Fabien Sanglard.

Articles Connexes

Enums vs Constantes : Pourquoi les Enums sont une solution plus sûre et plus intelligente

Enums vs Constantes : Pourquoi les Enums sont une solution plus sûre et plus intelligente

Utilisez-vous toujours des entiers ou des chaînes de caractères pour représenter des catégories fixes dans votre code ? Si c’est le cas, vous …

Lire la suite
Qu'est-ce qu'un Programme Informatique et un Langage de Programmation

Qu’est-ce qu’un Programme Informatique et un Langage de Programmation

À quoi peut-on comparer les programmes informatiques? Pour moi, ils sont comme des manuels d’instruction. D’un point de vue fonctionnel, …

Lire la suite
Qu'est-ce qu'une variable en programmation ? Guide simple pour débutants

Qu’est-ce qu’une variable en programmation ? Guide simple pour débutants

Comment les programmes stockent-ils et manipulent-ils des informations ? La réponse se trouve dans les variables, les éléments essentiels de la …

Lire la suite