Skip to content

1 1 3 5 texte

Nous allons maintenant étudier des techniques permettant de représenter des nombres qui ne sont pas entiers. Comme nous l’avons déjà observé, il est impossible, en toute généralité, de représenter tous les nombres réels à l’aide d’un ordinateur. En effet, les nombres irrationnels1, comme le nombre \(\pi\), ont une partie décimale infinie non-périodique2. Or tout ordinateur ne dispose jamais que d’une quantité finie de mémoire, et la représentation explicite d’un nombre réel irrationnel (dans n’importe quelle base naturelle) demanderait donc une mémoire infinie.

Les nombres que nous allons être en mesure de représenter sont tous des nombres rationnels, c’est-à-dire des nombres que nous exprimerons (dans une base fixée) sous la forme \(\alpha,\beta\) soit à l’aide d’une partie entière \(\alpha\) et d’une partie fractionnaire \(0,\beta\), toutes les deux finies. Par exemple, pour le nombre \(42,625\), nous avons \(\alpha=42\) et \(\beta=625\).

Une première technique: la virgule fixe

Comme nous l’avons déjà remarqué, si nous pouvons exprimer séparément \(\alpha\) et \(0,\beta\) en binaire, nous ne pouvons pas exprimer explicitement la virgule, et nous devons donc trouver une manière de contourner ce problème. Une première technique, qui est simple mais qui a ses limites, consiste à fixer arbitrairement la position de la virgule au sein d’une représentation de taille fixée. Par exemple, si on considère des représentations sur \(n=32\) bits, on pourrait décider que les \(16\) bits de poids fort représentent la partie entière \(\alpha\), et que les \(16\) bits de poids faible représentent la partie fractionnaire de \(0,\beta\).

Exemple :

Avec la convention ci-dessus, le nombre \(42,625\) est représenté par :

\[\begin{array}{|c|c|} \hline 0000\ 0000\ 0010\ 1010 & 1010\ 0000\ 0000\ 0000\\ \hline \end{array}\]

En effet, \(42,625_{10}=10\ 1010,101_2\). On remarque que des zéros on été ajoutés pour compléter les \(32\) bits : à gauche de la partie entière et à droite de la partie décimale.

\(\blacksquare\)

Le problème de cette technique est qu’elle limite de manière excessive les nombres qu’on peut représenter, comme le montre l’exemple suivant :

Exemple :

En suivant la convention donnée ci-dessus, le nombre \(2^{-17}\) ne peut pas être représenté. En effet :

\[2^{-17} = 0,0000\ 0000\ 0000\ 0000\ 1_2\ ,\]

et donc la partie \(\beta\) n’est pas représentable sur les \(16\) bits alloués dans notre représentation. On a affaire à un nombre trop petit, on parle d’underflow.

De même, le nombre \(2^{16}\) ne peut pas être représenté car :

\[2^{16} = 1\ 0000\ 0000\ 0000\ 0000_2 .\]

On a ici affaire à un overflow.

\(\blacksquare\)

Ces deux exemples sont un peu frustrants. On a en effet une représentation sur \(32\) bits, qui permettrait aisément de représenter tant \(2^{-17}\) que \(2^{16}\), si on avait l’opportunité de déplacer la virgule. En effet, tant la partie entière de \(2^{-17}\) que la partie décimale de \(2^{16}\) se réduisent à un seul \(0\), et ne nécessitent donc certainement pas \(16\) bits.

La virgule flottante: IEEE754

Pour remédier à ce problème, on peut utiliser une technique dite de virgule flottante, où la position de la virgule n’est pas spécifiée a priori, mais où la représentation binaire du nombre (tant sa partie entière que sa partie décimale) s’accompagne d’une information qui indique où positionner la virgule. Une telle technique s’inspire de la notation scientifique des nombres, qui consiste à exprimer tous les nombres (en base 10) sous la forme: \(\(0,f\times 10^x.\)\)

Exemple :

Voici trois nombres et leur représentations “scientifiques” respectives :

\[\begin{aligned} 42,625 &=& 0,42625\times 10^2\\ 0,00034 &=& 0,34 \times 10^{-3}\\ 25 &=& 0,25\times 10^2. \end{aligned}\]

\(\blacksquare\)

On voit bien sur ces exemples que l’exposant \(x\) indique la position de la virgule, ou, pour être plus précis, le nombre de décalages (vers la droite pour un exposant positif, vers la gauche pour un exposant négatif) de la virgule qu’il faut affecter au nombre \(0,\ldots\) pour retrouver le nombre d’origine.

Ce principe se retrouve dans le standard industriel IEEE754-20083, dont nous allons maintenant étudier une partie, à titre exemplatif (“IEEE Standard for Floating-Point Arithmetic” 2008). Nous allons nous concentrer sur la représentation des nombres sur \(32\) bits. Dans cette norme4, les \(32\) bits sont répartis entre :

  • un bit de signe \(s\), le bit de poids fort ;

  • suivi de \(8\) bits représentant un exposant \(e\), exprimé en excès à 127 ;

  • suivis de \(23\) bits de signifiant \(f\).

Une telle représentation est l’encodage du nombre suivant :

\[(-1)^s \times 1,f\times 2^e\ .\]

On voit donc que le bit \(s\) se comporte bien comme un bit de signe (le nombre représenté est négatif si et seulement si \(s=1\)). On suppose qu’on a préalablement exprimé le nombre sous la forme \(1,f\), et seuls les bits de \(f\) sont effectivement stockés dans la représentation, ce qui permet d’économiser un bit. Enfin, l’exposant \(e\) est une puissance de \(2\) et non pas de \(10\) comme dans la représentation scientifique, ce qui est logique étant donné que nous utilisons une représentation binaire. L’exposant indique donc bien un décalage à affecter à la virgule.

Exemple :

Considérons à nouveau le nombre \(42,625_{10}\). Pour trouver sa représentation IEEE754, nous commençons par l’exprimer en binaire :

\[42,625_{10} =10\ 1010,101_2\ .\]

Nous normalisons ensuite cette représentation en déplaçant la virgule de \(5\) positions vers la gauche pour obtenir un nombre de la forme \(1,f\times 2^e\) :

\[42,625_{10} =1,0101\ 0101 \times 2^{5}.\]

À noter que l’exposant est positif pour maintenir l’égalité. Nous pouvons maintenant trouver aisément les différents composants de la représentation :

  • le signe \(s=0\), car le nombre est positif ;

  • l’exposant \(e=5\), que nous devons représenter en excès à \(127\) sur \(8\) bits. Cela revient à représenter \(5+127=132\) en binaire, soit \(1000\ 0100\) ;

  • enfin, le signifiant est la partie après la virgule: \(f=01010101\).

Nous avons donc la représentation :

\[\begin{array}{|c|c|c|} \hline 0 & 1000\ 0100 & 0101\ 0101\ 0\cdots 0\\ \hline \end{array}\]

Remarquons que les \(0\) ont été ajoutés dans les bits de poids faible du signifiant, afin de ne pas changer sa valeur (ajouter des zéros dans les bits de poids forts reviendrait à insérer des zéros juste à droite de la virgule). ../.

Comme ces nombres sont relativement longs à écrire, il est souvent pratique d’utiliser une représentation en hexadécimal pour la totalité de l’encodage binaire:

\(0100\) \(0010\) \(0010\) \(1010\) \(1000\) \(0000\) \(0000\) \(0000\)
=
\(4\) \(2\) \(2\) \(\mathtt{a}\) \(8\) \(0\) \(0\) \(0\)

soit: \(422\mathtt{a}8000\).

\(\blacksquare\)

Un problème que nous devons encore résoudre est la représentation de \(0\). En effet, \(0\) ne peut pas s’exprimer sous la forme \(1,f\), il faut donc fixer une représentation spéciale. La norme IEEE754 en retient deux: \(10\cdots 0\) ou \(00\cdots 0\).

Notons enfin que la norme admet également des valeurs spéciales, comme \(\infty\) et Not a Number (ou NaN). Ces valeurs sont utilisées pour certains résultats des opérations arithmétiques. Par exemple pour une division par \(0\), comme on peut le voir dans le tableau suivant :

\(x/y\) \(y\neq 0,\infty,\textbf{NaN}\) \(y=\infty\) \(y=0\) \(y=\textbf{NaN}\)
\(x\neq 0,\infty,\textbf{NaN}\;\;\;\) valeur la plus proche de \(x/y\) 0 \(\infty\) NaN
\(x=\infty\) \(\infty\) NaN \(\infty\) NaN
\(x=0\) \(0\) \(0\) NaN NaN
 \(x=\textbf{NaN}\) NaN NaN NaN NaN

En pratique, la manipulation de cette représentation demande des circuits spéciaux, que l’on trouve sur la plupart des processeurs modernes.

Exemple :

Sur le processeur Intel 486 (I486 Microprocessor Programmer’s Reference Manual 1990), il est possible de manipuler des données en virgule flottante selon la norme IEEE754 (originelle), sur 32, 64 ou même 80 bits. Ces données doivent être chargées dans des registres spéciaux de 80 bits appelés st0,…st7. Des instructions dédiées comme fadd, fsub, fdiv, etc implémentent les opérations arithmétiques sur ces registres. Ces opérations ne sont pas réalisées par l’ALU, mais par un circuit dédié du processeur : le FPU (floating point unit), qui était un circuit séparé sur les processeurs Intel précédant le 486 (par exemple, pour le 386, il fallait acheter séparément un co-processeur appelé 387 pour disposer d’un FPU).

\(\blacksquare\)

I486 Microprocessor Programmer’s Reference Manual. 1990. Intel Corporation. http://bitsavers.trailing-edge.com/components/intel/80486/i486_Processor_Programmers_Reference_Manual_1990.pdf.

“IEEE Standard for Floating-Point Arithmetic.” 2008. IEEE. https://doi.org/10.1109/IEEESTD.2008.4610935.


  1. Il s’agit des nombres réels qui ne sont pas rationnels. 

  2. C’est-à-dire qu’elle ne peut pas être exprimée sous la forme d’un préfixe fini suivi d’une répétition infinie d’une séquence finie, car il est connu que tout nombre qui peut être exprimé de cette manière est rationnel. 

  3. L’IEEE est l’Institute of Electrical and Electronics Engineers, une association à but non-lucratif américaine regroupant des centaines de milliers de professionnels de l’électronique et de l’informatique. Elle conçoit et publie des normes qui peuvent ensuite être adoptées et mises en pratique par l’industrie. La norme IEEE754-2008 est la révision en 2008 de la norme 754 qui définit une représentation en virgule flottante adaptée aux ordinateurs. Notons qu'une nouvelle norme IEEE 754-2019 a été adoptée récemment ; celle-ci doit remplacer la norme IEEE 754-2008. 

  4. Le site web http://babbage.cs.qc.cuny.edu/IEEE-754/ implémente un convertisseur automatique qu’on peut utiliser pour se familiariser avec cette norme.