Aller au contenu

Principe de la virgule flottante

En cours

Ce cours est en cours d'élaboration. Il est donc déconseillé de le lire.

Jusqu'à maintenant, on a vu comment représenter des nombres entiers en binaire. Mais comment représenter des nombres réels ?

Nombres à virgule

Pour représenter des nombres réels en base 10, on ajoute une virgule, qui permet de rajouter des chiffres correspondant à des paquets de taille plus petits que 1. Par exemple, pour \(120,201\), on a les paquets suivants :

chiffre \(1\) \(2\) \(0\) \(,\) \(2\) \(0\) \(1\)
rang \(2\) \(1\) \(0\) - \(-1\) \(-2\) \(-3\)
taille paquet \(10^2\) \(10^1\) \(10^0\) - \(10^{-1}\) \(10^{-2}\) \(10^{-3}\)
taille paquet \(100\) \(10\) \(1\) - \(0,1\) \(0,01\) \(0,001\)

La partie à gauche de la virgule s'appelle la partie entière, et la partie à gauche la partie fractionnaire.

On peut faire exactement pareil avec des nombres binaires.

Par exemple, \(101,011_{2}\) se décompose de la manière suivante :

chiffre \(1\) \(0\) \(1\) \(,\) \(0\) \(1\) \(1\)
rang \(2\) \(1\) \(0\) - \(-1\) \(-2\) \(-3\)
taille paquet \(2^2\) \(2^1\) \(2^0\) - \(2^{-1}\) \(2^{-2}\) \(2^{-3}\)
taille paquet \(4\) \(2\) \(1\) - \(0,5\) \(0,25\) \(0,125\)

Conversion réel base 10 vers base 2

Pour convertir un nombre à virgule en base 2, de forme \(n,m\), on procède en deux étapes.

  • On commence par convertir la partie entière \(n\) (à gauche de la virgule) avec une des méthodes vues pour les entiers positifs.
  • On convertis ensuite la partie fractionnaire \(m\) (à droite de la virgule) de la manière suivante :
    • On commence avec \(p = m\)
    • On multiplie \(p\) par 2, ce qui donne \(q\).
    • Si \(q >= 1\), alors \(p = q - 1\), et on mémorise \(b = 1\)
    • Si \(q < 1\), alors \(p = q\), et on mémorise \(b = 0\)
    • Quand \(p = 0\), on s'arrête.
    • On lit alors les \(b\) dans l'ordre, et on obtient le codage binaire de la partie fractionnaire.

Exemple

Convertissons \(33,3125_{10}\) en base 2

On commence par calculer la représentation de \(33_{10}\) en base 2 : \(33_{10} = 100001_2\)

On applique ensuite la méthode ci-dessus avec \(p = 0,3125\) initialement.

\(p\) \(q = 2p\) \(q \geq 1\) ? \(q - 1\) \(b\)
\(0,3125\) \(0,625\) non - \(0\)
\(0,625\) \(1,25\) oui \(0,25\) \(1\)
\(0,25\) \(0,5\) non - \(0\)
\(0,5\) \(1,0\) oui \(0\) \(1\)
\(0\) stop - - -

donc \(0,3125_{10} = 0,0101_2\)

\[ 33,3125_{10} = 100001,0101_2 \]

Virgule fixe et précision

On a vu que les ordinateurs manipulent en général des nombres avec un nombre fixe de bits, en général 8, 16, 32 ou 64, pour des raisons de performances.

On pourrait décider d'allouer une partie des bits du nombres à sa partie fractionnaire. Par exemple, sur 16 bits, on aurait 8 bits pour la partie entière, et 8 bits pour la partie fractionnaire :

C'est ce que l'on appelle une représentation en virgule fixe. Elle a l'avantage de pouvoir réutiliser de nombreux circuits dédiés aux entiers, et est compatible avec la réprésentation en complément à deux. C'est souvent celle qui est utilisée dans les microcontroleurs et microprocesseurs destinés à l'informatique embarquée.

Notation scientifique

Bien entendu, la représentation en virgule fixe a aussi des défauts majeurs. La limite principale de la virgule fixe est qu'elle nécessite beaucoup, beaucoup de bits pour représenter des nombres très petits ou très grands. Si l'on voulait utiliser la représentation en virgule fixe pour manipuler des nombres réels pour faire du calcul scientifique. Par exemple la masse du soleil, d'environ

\[ 1\ 988\ 000\ 000\ 000\ 000\ 000\ 000\ 000\ 000\ 000\ \mathsf{kg} \]

et la constante gravitationnelle, d'environ

\[ 0,000\ 000\ 000\ 066\ 743\ \mathsf{m}^3\ \mathsf{kg}^{-1}\ \mathsf{s}^{-2} \]

il nous faudrait autour de 140 bits (100 pour la partie entière et 40 pour la partie flottante). Et il nous faut aussi beaucoup de chiffres même en base 10 !

On va donc utiliser une propritété pratique des calculs avec des grands ou petits nombres.

Imaginons que l'on ce soit trompé dans nos mesures, et qu'en réalité, le soleil est plus massif de 5 milliards de tonnes (1 tonne = 1000 kilogrammes). Sa masse serait alors :

\[ 1\ 988\ 000\ 000\ 000\ 000\ 005\ 000\ 000\ 000\ 000\ \mathsf{kg} \]

Vous avez probablement mis un peu de temps à repérer le 5, perdu au milieu de tous ces zéros. 5 milliards de tonnes, c'est tellement insignifiant par rapport à la masse totale du soleil, qu'il est raisonnable de considérer que cette erreur n'aurait eu aucun impact sur nos calculs. Donc, que la masse du soleil soit

\[ 1\ 988\ 000\ 000\ 000\ 000\ 005\ 000\ 000\ 000\ 000\ \mathsf{kg} \]

ou

\[ 1\ 988\ 000\ 000\ 000\ 000\ 000\ 000\ 000\ 000\ 000\ \mathsf{kg} \]

n'a dans la majorité des cas aucune importance. Ce qui est important, c'est quelques chiffres à la gauche du nombre (\(1\ 998\)), que l'on considère significatifs, et le nombre de chiffres que comporte le nombre et la position de la virgule, qui est l'ordre de grandeur. On peut alors exprimer l'ordre de grandeur sous forme d'une multiplication par une puissance de 10.

Par exemple, la masse du soleil s'écrira :

\[ 1,988 \times 10^{30}\ \mathsf{kg} \]

et la constante gravitationnelle :

\[ 6,6743 \times 10^{-11}\ \mathsf{m}^3\ \mathsf{kg}^{-1}\ \mathsf{s}^{-2} \]

C'est ce qu'on appelle la notation scientifique. Un nombre en notation scientifique est plus généralement structuré comme suit :

\[ \pm m \times 10^{n} \]

où :

  • \(\pm\) est le signe, \(+\) ou \(-\). Quand le signe est \(+\) on ne le note pas en général.
  • \(m\), avec \(m \in [1;10[\), la mantisse, qui exprime la partie significative de la valeur du nombre.
  • \(n\), avec \(n \in \mathbb{Z}\), l'exposant, qui exprime l'ordre de grandeur de la valeur du nombre.

La mantisse comporte toujours un chiffre dans sa partie entière, et peut en comporter plusieurs dans sa partie fractionnaire. On appelle ces chiffres les chiffres significatifs.

Parfois, \(\times 10^n\) est remplacé par \(\mathtt{e}n\) (e comme "exposant") minuscule ou majuscule : \(1,988\times 10^{30}\) s'écrit alors \(1,988\mathtt{e}30\).

Cette notation est extrêmement pratique :

  • Le nombre de chiffres significatifs (c'est à dire la précision du nombre) est évident, tout comme l'ordre de grandeur.
  • On peut représenter des très grands nombres avec peu de symboles. Dans de la masse du soleil, avec 4 chiffres significatifs, on a besoin que de 6 symboles : 4 pour la mantisse, et deux pour l'exposant. (On ne compte pas les autres symboles, comme la virgule ou le \(\mathtt{e}\), qui sont toujours présents dans la notation).

Virgule flottante

La notation scientifique est un cas particulier d'une représentation appelée virgule flottante, quel l'on abrègera FP, pour floating point, son nom anglais.

La mantisse représente un nombre avec pour ordre de grandeur 1, et l'exposant "déplace" la virgule dans ce nombre.

Illustration virgule flottante

D'où l'appellation virgule flottante. C'est aussi de là que vient le nom float, qui utilise une représentation similaire, mais en base 2.

En virgule flottante en base 2, un nombre est représenté par un triplet de trois nombres \((s; n; m)_{\mathtt{FP}}\):

  • \(s\) est le signe, et vaut 1 ou -1
  • \(m\) est la mantisse, en base 2, sur un nombre donné de bits. On ne considèrera que les cas avec un seul bit pour la partie entière.
  • \(n\) est l'exposant, un entier relatif en base 2, sur les bits restants.

La valeur du nombre représenté est :

\[ s \times m \times 2 ^{n} \]

Dans \(\times 2 ^{n}\), on a remplacé 10 par 2 par rapport à la notation scientifique, parce qu'en base 2, pour déplacer la virgule, il faut diviser ou multiplier par 2 et non par 10.

Conversion FP base 2 vers décimal

Pour trouver la valeur de \((s=-1_2; n=10_2; m=1,1001_2)_{\mathtt{FP}}\):

On commence par calculer les trois valeurs en représentation décimale.

\[ \begin{align} s &= -1_2 \\ &= -1_{10} \end{align} \]
\[ \begin{align} n &= 10_2\\ &= 2_{10} \end{align} \]
\[ \begin{align} m &= 1,1001_{2} \\ &= 1 \times 2^0 + 1 \times 2^{-1} + 1 \times 2 ^{-4}\\ &= 1 + \frac{1}{2} + \frac{1}{16}\\ &= 1,5625_{10} \end{align} \]

On applique ensuite la formule vue au dessus :

\[ \begin{align} (s; n; m)_{\mathtt{FP}} &= s \times m \times 2 ^{n}\\ (-1_2; 10_2; 1,1001_2)_{\mathtt{FP}} &= -1 \times 1,5625 \times 2 ^{2}\\ &= -6,25 \end{align} \]

Exercice : flottant vers décimal

Que vaut \((s=1_2; n=1110_2; m=1,0001_2)_{\mathtt{FP}}\) en décimal ?

Calculez les valeurs de \(s\), \(n\) et \(m\) en base 10, puis appliquez la formule du cours.

\[ s = 1_2 = 1_{10} \]
\[ n = 1110_2 = 8 + 4 + 2 = 14_{10} \]
\[ m = 1,0001_2 = 1 + 0,0625 = 1,0625_{10} \]

On applique la formule s \times m \times 2 ^{n}

\[ (s=1_2; n=1110_2; m=1,0001_2)_{\mathtt{FP}} = 1 \times 1,0625 \times 2^{14} = 17408_{10} \]

Conversion décimal vers FP base 2

Pour trouver la représentation en FP base 2 de \(-10,3125\) :

On commence par calculer la représentation binaire à virgule de \(-10,3125\), en convertissant successivement sa partie entière et sa partie fractionnaire.

Partie entière :

\[ 10_{10} = 1010_{2} \]

Conversion partie fractionnaire :

\(p\) \(q = 2p\) \(q \geq 1\) ? \(q - 1\) \(b\)
\(0,3125\) \(0,625\) non - \(0\)
\(0,625\) \(1,25\) oui \(0,25\) \(1\)
\(0,25\) \(0,5\) non - \(0\)
\(0,5\) \(1,0\) oui \(0\) \(1\)
\(0\) stop - - -
\[ 0,3125_10 = 0,0101_{2} \]

On déduis la représentation binaire à virgule de la valeur absolue du nombre :

\[ 10,3125_{10} = 1010,0101_{2} \]

On décale ensuite la virgule pour créer la mantisse de la représentation FP binaire, de sorte à ce que le 1 le plus à gauche soit l'unique chiffre de la partie entière. On compte les déplacements, dans le positifs si on déplace la virgule à gauche, dans le négatif si on déplace la virgule à droite, ce qui nous donne l'exposant. Dans notre cas, on fait trois déplacement à gauche de la virgule :

\[ 1010,0101_{2} = 1,0100101_{2} \times 2^{3} \]

On calcule la valeur de l'exposant en base 2:

\[ 3_{10} = 11_2 \]

On détermine finalement la valeur du signe, comme le nombre est négatif, le signe vaut \(-1_2\)

On peut maintenant reconstruire le triplet :

\[ -10,3125_{10} = (-1_2; 11_2 ;1,0100101_{2})_{\mathtt{FP}} \]

Qui corresponds au nombre :

\[ -1_2 \times 1,0100101_{2} \times 2^{11_2} \]

Exercice : conversion décimal vers flottant base 2

Déterminez \((s, m, n)_{\mathtt{FP}}\) pour représenter \(0,09375_{10}\) en virgule flottante.

N'oubliez pas de regarder le cours pour la méthode !

On commence par convertir \(0,09375_{10}\) en sa représentation base 2.

La partie entière de 0,09375_{10} est 0, sa conversion est donc triviale.

On convertis la partie fractionnaire.

\(p\) \(q = 2p\) \(q \geq 1\) ? \(q - 1\) \(b\)
\(0,09375\) \(0,1875\) non - \(0\)
\(0,1875\) \(0,375\) non - \(0\)
\(0,375\) \(0,75\) non - \(0\)
\(0,75\) \(1,5\) oui \(0,5\) \(1\)
\(0,5\) \(1\) oui \(0\) \(1\)
\(0\) stop - - -

La partie fractionnaire est donc \(0,09375_{10} = 0,00011_{2}\)

Donc \(0,09375_{10} = 0,00011_{2}\)

Déterminons la mantisse et l'exposant. Pour former la mantisse avec exactement un 1 dans la partie entière il faut décaler la virgule vers la droite 4 fois : \(0,00011_2 \rightarrow 00001,1_2\). L'exposant est donc \(-4\), et \(m=1,1_2\).

On convertis l'exposant en base 2 : \(-4_{10} = -100_2\)

Le nombre est positif, donc \(s=1\)

Donc :

\[ \begin{align} 0,09375_{10} &= (s=1_2; n=-100_2;m=1,1_2)_{\mathtt{FP}}\\ &= (1_2; -100_2;1,1_2)_{\mathtt{FP}} \end{align} \]

Précision de la représentation

A cause des restrictions matérielles des ordinateurs, on utilise généralement un nombre limité de bits pour coder les nombres réels en virgule flottante.

Malheureusement, le codage en virgule flottante dans les ordinateurs, comme toute représentation avec un nombre limité de chiffres, ne permet pas de coder toutes les valeurs réelles dans un interval donné.

De plus, le codage binaire, tout comme les représentations décimales, ne permet pas de coder toutes les valeurs sur un nombre fini de chiffres.

Essayons par exemple de convertir \(0,2_{10}\) en représentation binaire.

\(p\) \(q = 2p\) \(q \geq 1\) ? \(q - 1\) \(b\)
\(0,2\) \(0,4\) non - \(0\)
\(0,4\) \(0,8\) non - \(0\)
\(0,8\) \(1,6\) oui \(0,6\) \(1\)
\(0,6\) \(1,2\) oui \(0,2\) \(1\)
\(0,2\) \(0,4\) non - \(0\)

On remarque qu'après quelques étapes, on retombe sur \(p = 0,2\). On peut donc continuer à l'infini. alors,

\[ 0,2_{10} = 0,0011001100110011\cdots _{2} \]

\(\cdots\) représente une répétition infinie de \(0011\).

On ne peut donc pas écrire \(0,2\) en représentation binaire avec un nombre fini de bits.

On doit alors accepter de faire une approximation, c'est à dire de représenter \(0,2\) par un nombre proche. Par exemple, sur 9 bits, on peut choisir \(0,00110011_2\), ce qui vaut :

\[ \begin{align} 0,00110011_2 &= 1 \times 2^{-3} + 1 \times 2^{-4} + 1 \times 2^{-7} + 1 \times 2^{-8}\\ &= \frac{1}{8} + \frac{1}{16} + \frac{1}{128} + \frac{1}{256}\\ &= 0,1992\ 1875 \end{align} \]

Plus généralement, un interval donné entre deux réels non égaux, contient une infinité de valeurs. Or, avec \(m\) bits, on peut représenter au maximum \(2^m\) valeurs.

Considérons le cas où la mantisse est toujours de la forme \(1,b_{-1},b_{-2},b_{-3}\), c'est à dire qu'elle est sur 4 bits et que son bit de partie entière est toujours à 1.

Si l'exposant vaut 0, les valeurs représentées sont alors comprises dans l'interval

\[ [2^0, 2^{0+1}[ \]

Le nombre représenté est la mantisse elle-même, sans décalage. La plus petite valeur est donc \(2^0 = 1\), qui corresponds à la mantisse avec tous les bits de sa partie fractionnaire à zéro.

Si on met le bit de poids faible de la mantisse à 1, on ajoute \(2^{-3}\) au nombre. On ne peut donc que représenter des valeurs multiples de \(2^{-3}\) sur cet interval.

Reprenons le même raisonnement dans le cas où l'exposant vaut 4. les valeurs représentées sont alors comprises dans l'interval

\[ [2^4,2^{4+1}[ \]

La plus petite valeur étant \(2^4\), qui corresponds à une mantisse avec tous les bits de sa partie fractionnaire à 0 : \(1,000_2\).

Si on met le bit de poids faible de la mantisse à 1, on ajoute \(2^{4 - 3} = 2\) au nombre. On ne peut donc que représenter des valeurs multiples de 2 sur cet interval.

D'une manière plus générale, pour une mantisse à \(m\) bits dans sa partie fractionnaire, et pour un exposant qui vaut \(n\), on représentera des valeurs dans l'intervalle

\[ [2^n,2^{n+1}[ \]

La plus petite valeur étant \(2^n\), et toutes les valeurs suivantes dans l'intervalle étant des multiples de \(2^{n - m}\). On appelle l'écart maximal entre une valeur réelle quelconque et le réel le plus proche représentable dans une représentation flottante la précision de la représentation.

On peut représenter ces valeurs sur la droite des réels :

TODO (prof) ici ajouter une représentation graphique de la répartition des valeurs

On remarque que la précision dépends de la taille du nombre représenté. Ainsi, les nombres très petits sont représentés avec une très grande précision, alors que les très grands nombres sont représentés avec une précision bien plus petite. Cette propriété rends cette représentation adaptée à la plupart des cas pratiques de calcul avec des réels, parce comme on l'a vu tôt dans le chapitre, on n'a rarement besoin d'une très grande précision pour effectuer des calculs sur des très grands nombres.

Limitation

Les représentations flottantes, malgré leurs avantages, ont aussi des inconvénients.

Du fait de l'approximation, les calculs effectués sur des virgules flottantes peuvent donner des résultats légèrement imprécis.

Par exemple, l'expression Python suivante, qui utilise des nombres en représentation à virgule flottante :

0.3 * 0.2 * 10.0 == 6.0

vaut False, alors que son équivalent mathématique :

\[ 0,3 \times 0,2 \times 10 = 6 \]

est vraie. Celà vient du fait que \(0,3\) et \(0,2\) ne peuvent être représentés exactement sur un nombre finis de bits en virgule flottante base 2.

Il faut donc éviter de tester l'égalité (opérateur ==) entre deux nombre en virgule flottante quand on programme, mais à la place tester un encadrement :

a = 0.3 * 0.2 * 10.0
t = 0.00001 ## e pour tolérance
r = 6.0 - t <= a <= 6 + t
print(r)

Affiche True.

Le seul moment où un test d'égalité entre deux nombre en virgule flottante est si l'on veut éviter une valeur particulière, par exemple une division par zéro :

dividende = input("Dividende : ")
diviseur = input("Diviseur : ")

if diviseur == 0.0: ## ici, le test d'égalité est justifié, parce qu'on veut précisément eviter la valeur zéro
    print("Division par 0 impossible !")
else :
    print(dividende + "/" + diviseur + " = " + dividende/diviseur)