2. Nombres flottants

2.1. Introduction

Les nombres réels ne sont pas représentables en totalité en informatique en raison de la forme et de la taille de la représentation utilisée. Nous allons voir que nous ne pouvons en coder qu'une partie. On utilise une notation spécifique issue de la norme IEEE 754. Les nombres qui résultent de cette transformation sont appelés nombres flottants ou nombres à virgule flottante.

En informatique, on note les nombres flottants sous la forme suivante :

  • zéro : $0.0$, ou parfois $0$ comme pour les entiers, la conversion sera réalisée par le compilateur
  • $3.14159265$, la virgule est remplacée par le point, il s'agit de la notation anglo-saxonne
  • $1.89e-3 = 0.00189$, le caractère 'e' ou 'E' représente l'exposant, c'est à dire $10^n$, avec ici $n=-3$

Dans la norme IEEE 754, il existe une modélisation qui utilise 32 bits, appelée également simple précision. La double précision utilise quant à elle 64 bits.

Nous allons étudier la simple précision qui correspond au type float du langage C. Elle utilise :

  • 1 bit de signe $S$, le bit de poids fort (bit 31) qui vaut 0 pour un nombre positif et 1 pour un nombre négatif
  • 8 bits pour coder l'exposant décalé (ou biaisé) $E_d$, car par convention on lui ajoute 127
  • 23 bits pour coder la mantisse tronquée $M_t$, elle est dite tronquée car on a supprimé le premier bit du nombre qui vaut forcément 1 pour tout nombre différent de 0
S Exposant Décalé Mantisse Tronquée
1 1 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0
1 8 bits 23 bits

Le nombre réel $n$ qui correspond au nombre flottant s'exprime alors sous la forme :

$$ n = (-1)^S × 1,M_t × 2^(E_d - 127) $$

On peut alors coder des nombres entre ± $3,4027 × 10^{38}$ et ± $1,1754 × 10^{-38}$.

Par exemple :

Vous pouvez utiliser l'IEEE 754 Converter pour représenter des nombres flottants.

2.1.1. Coder un réel en flottant

Comment coder un nombre réel au format IEEE 754 ? Prenons l'exemple de la représentation en simple précision sur 32 bits du codage de $n = -1027,625$ ci dessous.

On procède comme suit :

  • il s'agit d'un nombre négatif donc $S = 1$
  • on code la partie entière en valeur absolue : $$1027_{10} = 1024_{10} + 2_{10} + 1_{10} = 2^{10} + 2^{1} + 2^{0} = 100\_0000\_0011_{2}$$
  • on code la partie décimale en utilisant des puissances de 2 négatives : $$0,625 = 0,5 + 0, 125 = 2^{-1} + 2^{-3}$$

La mantisse qui regroupe partie entière et décimale est alors:

$$M = 100\_0000\_0011,101_{2}$$

Pour obtenir la mantisse tronquée et l'exposant décalé, il suffit de déplacer la virgule vers la gauche derrière le premier 1 qui compose la mantisse, on parle alors de normalisation du nombre à représenter :

$$1,0000\_0000\_1110\_1_{2}$$

Par conséquent, on a déplacé la virgule de 10 rangs vers la gauche (cf. image ci-dessus) ce qui correspond à l'exposant $E = 10$.

  • la mantisse tronquée est alors égale à la mantisse à laquelle on a enlevé le premier 1 devant la virgule, on obtient donc $M_t = 0000\_0000\_1110\_1_{2}$
  • l'exposant décalé est égal, par convention en 32 bits, à $127 + E$, dans notre cas $E = 10$, donc : $$E_d = 127 + 10 = 137_{10} = 1000\_1001_{2}$$

On remplit alors chacun des champs du nombre flottant et on complète la mantisse tronquée par des zéros à droite. Au final on obtient une valeur sur 32 bits que l'on exprime généralement en hexadécimal pour plus de lisibilité. On obtient donc $C4\_80\_74\_00_{16}$.

S Exposant Décalé Mantisse Tronquée
1 1 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0
C 4 8 0 7 4 0 0

2.1.2. Comment coder la partie décimale ?

Imaginons que nous ayons à coder $0,4$, comment procéder ?

Il existe un algorithme simple pour réaliser le codage :

Cette méthode consiste à multiplier la partie décimale par $2$ jusqu'à obtenir 0 quand cela est possible ou, si on ne s'arrête pas, à obtenir assez de chiffres pour remplir la mantisse.

A chaque étape on garde le chiffre le plus à gauche du résultat (c'est à dire la partie entière) de la multiplication qui sera $1$ ou $0$, puis, on réitère la multiplication sur la partie décimale du résultat de la multiplication en supprimant le premier $1$ s'il existe.

Vous pouvez tester avec un exemple :

 



Exercice 2.1

Essayer de convertir $0,3$, $0,6$ ou $0,9$ en utilisant cet algorithme. Que remarquez vous ?

2.1.3. IEEE 754 64 bits et les autres précisions

Pour approfondir...

Pour la modélisation en 64 bits, dite double précision, on utilise :

  • toujours 1 bit pour le signe comme en 32 bits
  • 11 bits pour l'exposant, on ajoute 1023 pour obtenir l'exposant décalé
  • 52 bits pour la mantisse tronquée

Enfin il existe :

  • la demi précision qui occupe 16 bits
  • la simple précision étendue (40 bits), la double précision étendue (80 bits)
  • la quadruple précision qui utilise 128 bits
Caractéristiques des représentations IEEE 754
Signe (bits) $1$ $1$ $1$ $1$
Exposant (bits) $5$ $8$ $11$ $15$
Mantisse (bits) $11$ $23$ $52$ $113$
Plus petit nombre $± 6,103 10^{-5}$ $± 1,175 10^{-38}$ $± 2,225 10^{-308}$ $± 3.362 10^{-4932}$
Plus grand nombre $± 65504$ $± 3,402 10^{38}$ $± 1,797 10^{308}$ $± 1.189 10^{4932}$
Décimales $3$ $7$ $16$ $34$

2.1.4. Erreurs de précision

Lorsque l'on utilise la représentation IEEE 754, on rencontre deux problèmes liés à la précision des valeurs codées :

Comme on utilise que des puissances de 2 négatives qui se terminent par 5, on ne peut donc coder la plupart des nombres décimaux qu'en utilisant une combinaison de puissances de 2 négatives et cela engendre des erreurs de précision :

Vous pouvez utiliser le formulaire suivant pour calculer la somme des valeurs cochées ou chercher une valeur approchée d'une valeur spécifique comprise entre 0 et 1,0. Vous pouvez essayer avec 0,3 ou 0,33 par exemple.

nSel.2^n
-10.50000000000000000000000
-20.25000000000000000000000
-30.12500000000000000000000
-40.06250000000000000000000
-50.03125000000000000000000
-60.01562500000000000000000
-70.00781250000000000000000
-80.00390625000000000000000
-90.00195312500000000000000
-100.00097656250000000000000
-110.00048828125000000000000
-120.00024414062500000000000
-130.00012207031250000000000
-140.00006103515625000000000
-150.00003051757812500000000
-160.00001525878906250000000
-170.00000762939453125000000
-180.00000381469726562500000
-190.00000190734863281250000
-200.00000095367431640625000
-210.00000047683715820312500
-220.00000023841857910156250
-230.00000011920928955078125
Somme des nombres sélectionnés

Valeur approchée

Cherche une valeur approchée strictement inférieure ou égale à la valeur recherchée





Gestion des nombres
  


L'autre problème lié à la précision provient du fait que la taille de la mantisse peut être trop petite pour représenter certains nombres qui comportent beaucoup de chiffres, notamment en 32 bits, car on dispose de 7 chiffres significatifs.

Pour approfondir...

C'est pour cela que le coprocesseur arithmétique, que l'on appelle également FPU (Floating Point Unit) qui réalise les opérations sur les nombres flottants, utilise un codage sur 80 bits afin de minimiser les erreurs de précision.

Un processeur contient plusieurs coeurs de calcul et chaque coeur possède une ou plusieurs FPU.

2.1.4.a  Exemple

On peut voir sur le listing suivant un exemple de code très simple qui réalise la différence entre des valeurs flottantes proches.

  1. #include <iostream>
  2. #include <iomanip>
  3. #include <cmath>
  4.  
  5. // Définition de variables globales
  6. float v1 = 1.2;
  7. float v2 = 1.3;
  8. float v3 = 1.3001;
  9. float v4 = 1.3001001;
  10.  
  11. /* ------------------------------------------------------------------
  12.     QUOI
  13.        Fonction principale
  14.        
  15.    ------------------------------------------------------------------ */
  16. int main() {
  17.  
  18.   float diff_v1_v2 = v1 - v2;
  19.   float diff_v2_v3 = v2 - v3;
  20.  
  21.   std::cout << setprecision(10);
  22.  
  23.   std::cout << "v1-v2 = " << diff_v1_v2 << std::endl;
  24.   std::cout << "v2-v3 = " << diff_v2_v3 << std::endl;
  25.  
  26.   // Comparaison de valeurs flottantes
  27.   float diff_abs = fabs(v3 - v4);
  28.   std::cout << "|v3-v4| = " << diff_abs << std::endl;
  29.  
  30.   if (diff_abs < 1E-6)
  31.     std::cout << "v3 = v4" << std::endl;
  32.   else
  33.     std::cout << "v3 != v4" << std::endl;
  34.   return 0;
  35. }
  36.  

Cependant, le résultat de l'exécution ne correspond pas à ce que nous devrions obtenir :

v1-v2 = -0.09999990463     ! et non -0.1
v2-v3 = -0.0001000165939   ! et non -0.0001
|v3-v4| = 1.192092896e-07  ! et non  0.0000001
v3 = v4 

Cela est dû au fait qu'il est impossible de coder exactement certaines valeurs comme nous l'avons expérimenté pour représenter $0,3$.

Le problème lié aux erreurs de précision implique que pour comparer deux valeurs en virgule flottante on ne peut pas utiliser l'opérateur d'égalité (==) du langage C comme on le ferait pour des entiers, il est nécessaire d'utiliser la valeur absolue de la différence des deux valeurs (lignes 23 et 26 du listing précédent) et de vérifier que cette différence est bien inférieure à un $ε$ donné.

On doit donc écrire :

// permet de comparer deux float
if (fabs(x1 - x2) < 1e-6) {
    // égalité
}

au lieu de :

// ne permet pas de comparer deux float
if (x1 == x2) {
    // égalité
}

Si on utilise une précision plus grande de 64 bits, c'est à dire un double en langage C, on obtient un résultat qui correspond à un calcul exact :

v1-v2 = -0.1
v2-v3 = -0.0001
|v3-v4| = 1.000000001e-07
v3 = v4

Néanmoins, on obtiendra les mêmes erreurs de précision dès lors que les nombres à traiter possèdent un nombre de chiffres après la virgule important qui dépasse la capacité de représentation des nombres en double précision.

Pour approfondir...

Si on désire faire des calculs exacts, il existe une librairie dédiée appelée GMP pour The GNU Multiple Precision Arithmetic Library.