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 :
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 sous la forme :
$$ 1,0101... × 2^E $$où la mantisse $1,0101...$ est une suite quelconque de $0$ et de $1$, et $E$ est l'exposant.
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 → |
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.
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 :
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$.
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 |
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.
Pour les nombres positifs :
si $x < y$ alors $x' < y'$
Pour les nombres négatifs :
si $x < y$ alors $x' > y'$
pour les nombres positifs :
Nombre | Hexadécimal |
0.001 | 3A_83_12_6F |
0.290 | 3E_94_7A_E1 |
0.300 | 3E_99_99_9A |
0.310 | 3E_9E_B8_52 |
100.000 | 42_C8_00_00 |
1.000.000 | 44_7A_00_00 |
x_1 | comp | x_2 | comp | x_3 | comp | x_4 |
$0,001$ | < | $0,29$ | < | $10^2$ | < | $10^3$ |
3A_83_12_6F | < | 3E_94_7A_E1 | < | 42_C8_00_00 | < | 44_7A_00_00 |
981_668_463 | < | 1_049_918_177 | < | 1_120_403_456 | < | 1_148_846_080 |
pour les nombres négatifs :
Nombre | Hexadécimal |
-0.001 | BA_83_12_6F |
-0.290 | BE_94_7A_E1 |
-0.300 | BE_99_99_9A |
-0.310 | BE_9E_B8_52 |
-100.000 | C2_C8_00_00 |
-1000.000 | C4_7A_00_00 |
x_1 | comp | x_2 | comp | x_3 | comp | x_4 |
$-10^3$ | < | $-10^2$ | < | $-0,29$ | < | $-0,001$ |
C4_7A_00_00 | > | C2_80_00_00 | > | BE_94_7A_E1 | > | BA_83_12_6F |
3_296_329_728 | > | 3_263_168_512 | > | 3_197_401_825 | > | 3_129_152_111 |
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 ?
Pour approfondir...
Pour la modélisation en 64 bits, dite double précision, on utilise :
Enfin il existe :
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$ |
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 |
---|---|---|
-1 | 0.50000000000000000000000 | |
-2 | 0.25000000000000000000000 | |
-3 | 0.12500000000000000000000 | |
-4 | 0.06250000000000000000000 | |
-5 | 0.03125000000000000000000 | |
-6 | 0.01562500000000000000000 | |
-7 | 0.00781250000000000000000 | |
-8 | 0.00390625000000000000000 | |
-9 | 0.00195312500000000000000 | |
-10 | 0.00097656250000000000000 | |
-11 | 0.00048828125000000000000 | |
-12 | 0.00024414062500000000000 | |
-13 | 0.00012207031250000000000 | |
-14 | 0.00006103515625000000000 | |
-15 | 0.00003051757812500000000 | |
-16 | 0.00001525878906250000000 | |
-17 | 0.00000762939453125000000 | |
-18 | 0.00000381469726562500000 | |
-19 | 0.00000190734863281250000 | |
-20 | 0.00000095367431640625000 | |
-21 | 0.00000047683715820312500 | |
-22 | 0.00000023841857910156250 | |
-23 | 0.00000011920928955078125 | |
-24 | 0.00000005860464477539062 |
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.
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.
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.
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 les entiers :
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
Pour les flottants :
L'affichage donnera :
a = 31415926535897932384626433832795028841971693993751e-2
b = 27182818284590452353602874713526624977572470936999e-2
a + b = 5859874482048838473822930854632165381954416493075e-2
a + b = 0.05859874482048838473822930854632165381954416493075
a * b = 85397342226735670654635508695465744950348885357651e-4
a * b = 0.00085397342226735670654635508695465744950348885358