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 consiste à représenter un nombre $N$ en base 10 sous une forme binaire :

Si $N = 0$ alors on aura 32 bits à la valeur 0, par contre si $N ≠ 0$ :

$$ N_{10} = M × 2^E $$

où la mantisse $M$ est une suite de $0$ et de $1$ qui représente le codage binaire de $N$, et $E$ est l'exposant associé à $N$.

  • 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
Signe 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 bit ← 8 bits → ← 23 bits →
C 4 8 0 7 4 0 0
-1027.625

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. Pourquoi ajouter 127 à l'exposant ?

The eight-bit exponent uses excess 127 notation. What this means is that the exponent is represented in the field by a number 127 greater than its value. Why? Because it lets us use an integer comparison to tell if one floating point number is larger than another, so long as both are the same sign.

Soit $x$ et $y$ deux nombres décimaux et $x'$ et $y'$ leurs représentations à virgule flottante interprétée sous forme d'entier non signé.

en fonction du signe, si $x <= y$ alors $x' <= y'$ ou alors $x' >= y'$

 Nombre
$x$ 
 Hexadécimal
$x'$ 
 -100000.000000   c7c35000 
 -1000.000000   c47a0000 
 -100.000000   c2c80000 
 -10.000000   c1200000 
 -1.000000   bf800000 
 -0.000010   b727c5ac 
 0.000000   0 
 0.000010   3727c5ac 
 1.000000   3f800000 
 10.000000   41200000 
 100.000000   42c80000 
 1000.000000   447a0000 
 100000.000000   47c35000 
Exemples de valeurs

Par exemple, si on compare :

2.1.3. 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 de la partie entière $z$ et on réitère le processus sur $(2x -z)$.

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.4. 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
Précision : demi simple double quadruple
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.5. 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.

n Sel. 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
-240.00000005860464477539062
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.5.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 27 et 30 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.

2.1.5.b  GNU Multi precision Library

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.

Pour installer la librairie GMP procéder comme suit sous Ubuntu :

sudo apt install build-essential m4
tar -xf gmp-6.3.0.tar.xz 
cd gmp-6.3.0/
./configure --build=`config.guess` --enable-cxx
make -j4
sudo make install
sudo ldconfig

α)  Pour les entiers

  1. // compiler avec g++ -o gmp_integer_example.exe gmp_integer_example.cpp -O3 -lgmp
  2. #include <gmp.h>
  3. #include <string>
  4. #include <iostream>
  5. #include <algorithm>
  6. using namespace std;
  7.  
  8. // fonction qui transforme une chaine de la forme 1289347884
  9. // en 1_289_347_884
  10.  
  11. string separateur(const string& input, char separator = '_') {
  12.     string result;
  13.     int count = 0;
  14.  
  15.     // Iterate the string in reverse order
  16.     for (int i = input.size() - 1; i >= 0; --i) {
  17.         result.push_back(input[i]);
  18.         count++;
  19.  
  20.         // Insert separator every 3 digits, but not after the last group
  21.         if (count == 3 && i != 0) {
  22.             result.push_back(separator);
  23.             count = 0;
  24.         }
  25.     }
  26.  
  27.     // Reverse the result string back to correct order
  28.     std::reverse(result.begin(), result.end());
  29.     return result;
  30. }
  31.  
  32. ostream& operator<<(ostream& out, mpz_t& z) {
  33.     char* str = mpz_get_str(nullptr, 10, z);  
  34.     out << separateur( str );  // Output the string to the stream
  35.     free(str);
  36.     return out;
  37. }
  38.  
  39. int main() {
  40.     mpz_t a, b;
  41.    
  42.     mpz_init(a);
  43.     mpz_init(b);
  44.    
  45.     // calculer 2^64 en multipliant a=1 par b=2 64 fois
  46.     mpz_set_ui(a, 1);
  47.     mpz_set_ui(b, 2);
  48.    
  49.     for (int i = 1; i <= 64; ++i) {
  50.         mpz_mul(a, a, b);
  51.     }
  52.    
  53.     cout << "2^64 = " << a << endl;
  54.    
  55.     // utiliser la fonction puissance pour calculer 2^128
  56.     mpz_ui_pow_ui(a, 2, 128);
  57.     cout << "2^128 = " << a << endl;
  58.    
  59.     // utiliser la fonction puissance pour calculer 2^32
  60.     mpz_ui_pow_ui(a, 2, 32);
  61.     cout << "2^32 = " << a << endl;
  62.    
  63.    
  64.     mpz_clear(a);
  65.     mpz_clear(b);
  66.    
  67.     return EXIT_SUCCESS;
  68. }
  69.  

L'affichage donnera :

2^64 = 18_446_744_073_709_551_616
2^128 = 340_282_366_920_938_463_463_374_607_431_768_211_456
2^32 = 4_294_967_296
  1. #include <iostream>
  2. #include <gmpxx.h>
  3.  
  4. int main() {
  5.     // Version utilisant la syntaxe C++
  6.     // ainsi que l'opérateur '*'
  7.    
  8.     // Declare an arbitrary precision integer class
  9.     mpz_class factorial = 1;
  10.  
  11.     // Loop from 1 to 50, multiplying 'factorial' by the current number
  12.     for (int i = 1; i <= 50; i++) {
  13.         factorial *= i;
  14.     }
  15.  
  16.     // Print the result. The overloaded << operator handles printing
  17.     std::cout << "The factorial of 50 is:\n" << factorial << std::endl;
  18.  
  19.     return 0;
  20. }
  21.  
  22.  
richer@solaris:~$ g++ -o gmp_factorial.exe gmp_factorial.cpp -lgmpxx -lgmp
richer@solaris:~$ ./gmp_factorial.exe 
The factorial of 50 is:
30414093201713378043612608166064768844377641568960512000000000000

β)  Pour les flottants

Calcul de Pi avec la série de Leibniz :

  1. #include <iostream>
  2. #include <iomanip>
  3. #include <gmpxx.h>
  4.  
  5. /**
  6.  * Calcul de Pi, exemple donné par Gemini de Google
  7.  * Utilisation de la série de Leibniz
  8.  *
  9.  *  Pi/4 = 1 -1/3 + 1/5 -1/7 + 1/9 + ...
  10.  */
  11. int main() {
  12.     mpf_set_default_prec(200);
  13.  
  14.     mpf_class pi = 0;
  15.     mpf_class term;
  16.  
  17.     long long num_terms = 100000000;
  18.  
  19.     for (long long n = 0; n < num_terms; ++n) {
  20.         if (n % 2 == 0) {
  21.             term = 1;
  22.         } else {
  23.             term = -1;
  24.         }
  25.  
  26.         // Convertir explicitement le résultat en long int avant de construire mpf_class
  27.         mpf_class denominator = static_cast<long int>(2 * n + 1);
  28.         term /= denominator;
  29.  
  30.         pi += term;
  31.     }
  32.  
  33.     pi *= 4;
  34.  
  35.     std::cout << "Pi avec la série de Leibniz (" << num_terms << " termes) est :\n";
  36.     std::cout << std::fixed << std::setprecision(50) << pi << std::endl;
  37.  
  38.     return 0;
  39. }
  40.  

L'affichage donnera :

Pi avec la série de Leibniz (100000000 termes) est :
3.14159264358979323846264363327950288419713814937511