PTSI1 – 2016/2017 – INFO Lycée La Martinière-Monplaisir – Lyon Chapitre 1010. Représentation des nombres. Nous avons vu au chapitre 1, qu’en informatique, les données sont stockées sous forme de bits (binary digit en anglais). Nous allons maintenant détailler la façon dont on traduit les nombres sous cette forme. 1 Représentation des nombres entiers 1.1 Écriture d’un nombre entier naturel en base k H L’écriture 1984 exprime un entier naturel formé de 4 unités, 8 dizaines, 9 centaines et 1 millier : 1984 = Le choix de faire des paquets de 10 est cependant arbitraire : on peut tout aussi bien décider de faire des paquets de 2, 5, 8, 16, 20 (chez les Mayas), 60 (chez les Babyloniens)... Si on choisit de faire des paquets de k, on dit qu’on décompose un nombre en base k. H Décomposons par exemple 45 en base 10, en base 5 et en base 2 : 45 = = = Remarque : Vu l’importance de la base 2, il est bon de connaître les premières puissances de 2 : 20 21 22 23 24 25 26 27 28 29 210 On adopte parfois les notations suivantes : on écrit la suite des chiffres qui permet d’exprimer le nombre dans la base k et on met k en indice : 4510 = = On distingue aussi la représentation binaire (en base 2) en la soulignant : 45 = . Remarque : Tout comme on peut additionner deux nombres écrits en base 10 en utilisant l’algorithme appris à l’école primaire, on peut effectuer l’addition entre deux nombres écrits avec la représentation binaire. Par exemple, 310 + 610 = 910 en décimal. En binaire, 1 H Lorsqu’on veut écrire un nombre dans une base k avec k > 10, il nous "manque" en quelque sorte des unités. Il faut les inventer ; par exemple, pour la base 16 (ou base hexadécimale, très utilisée en informatique : elle apparaît dans les clés wifi), les chiffres "hexadécimaux" sont : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A (dix), B (onze), C (douze), D (treize), E (quatorze), F (quinze) On a 45 = 1.2 Algorithme de décomposition Sur ces exemples assez simples, on peut s’en sortir comme cela, en tâtonnant. La méthode générale pour décomposer un entier n en base k est la suivante : – On effectue des divisions euclidiennes successives par k jusqu’à avoir un quotient nul ; – On récupère les restes en remontant les calculs. Illustrons par un exemple avec x = 58 et k = 5. 58 = = = En combinant toutes les lignes : 58 = = Exercice : Avec cette méthode, déterminer l’écriture binaire de 22. 2 Il s’agit là d’un algorithme, qu’on peut décrire de la façon suivante : Entrées: x, k entiers Sorties: décomposition de l’entier x dans la base k n←x E ← mot vide tantque n > 0 faire q ← quotient dans la division euclidienne de n par k r ← reste dans la division euclidienne de n par k Ajouter r devant le mot E n←q fin tantque Renvoyer le mot E 1.3 La représentation des entiers naturels Rappelons que la mémoire des ordinateurs est constituée de circuits ne pouvant être que dans deux états ; l’un est identifié à 0 et l’autre à 1, les deux chiffres en base deux, ce qui justifie l’appellation bit (binary digit c’est-à-dire chiffre binaire en anglais). Il est donc naturel de représenter les entiers naturels dans la mémoire des ordinateurs par des bits contenant leur décomposition en base 2. Par exemple, 13 = donc il faut bits pour coder 13, il s’écrira en machine : Remarque : Avec n bits, on peut représenter tous les entiers de 0 à l’entier représenté en base 2 par 11 . . . 1 = | {z } n bits 1.4 Cas des entiers relatifs H Codage naïf (non retenu) : Pour représenter les entiers relatifs, une solution est d’ajouter un bit donnant le signe : par exemple 0 pour les positifs ou nul et 1 pour les négatifs. Défauts : – 0 a deux représentations : – L’addition binaire n’est plus respectée. 3 H Codage utilisé : le complément à deux Pour n = 8 bits, cette méthode va permettre de coder les entiers entre −27 = −128 et 27 −1 = 127. • Si l’entier x à coder est positif ou nul : • Si l’entier x à coder est négatif, entre −128 = −27 et −1 : Généralisons avec n bits. On pourra représenter les entiers relatifs x entre −2n−1 et 2n−1 − 1. – Si x ≥ 0 alors x est représenté par sa notation binaire. – Si x < 0 alors x est représenté par la notation binaire de x + 2n , qui est un entier entre 2n−1 et 2n − 1. +2n 2n codes 0 −2n−1 ... −1 2n−1 entiers 2n−1 − 1 1 ... 2n−1 2n − 1 2n−1 entiers 2n−1 entiers 2n entiers à représenter Exemple : donner la représentation de 67, de −31 et de 0 avec cette convention (sur 8 bits). 4 Remarque : Le langage Python ne connaît pas le type entier naturel. Ainsi, un entier naturel est considéré comme un entier relatif. 1.5 Dépassement de capacité et entiers longs Les langages de programmation se fixent en général un nombre d’octets sur lesquels ils vont coder les entiers. Par exemple, par défaut, en Python, c’est 4 octets, c’est-à-dire 32 bits, d’où une plage d’entiers de −2 147 483 648 à 2 147 483 647. Si l’on dépasse ces limites, on passe automatiquement, en Python, à un autre type que le type entier : le type long ("entiers longs"). La représentation des entiers n’est donc en pratique limitée que par la taille de la mémoire disponible en machine. 2 Représentation des nombres réels 2.1 Approximation binaire d’un réel de l’intervalle [0, 1[ H Pour écrire un nombre réel x de façon approchée, nous avons l’habitude d’utiliser l’écriture décimale de x et de la tronquer (ou de faire un arrondi) ; par exemple, une valeur approchée de π est 3, 14 (à 10−2 près). Ainsi le nombre π − 3 a pour valeur approchée 0, 14 = 4 1 + 2 (à 10−2 près), et c’est un réel 10 10 de [0, 1[. H Généralisons : tout réel x ∈ [0, 1[ aura une valeur approchée (à 10−N près) de la forme : b1 b2 bN + + ··· + N 101 102 10 où b1 , b2 , ... , bN sont des entiers entre 0 et 9 : x ≈ On peut faire exactement la même chose en remplaçant les "10" par des "2". Tout réel x ∈ [0, 1[ aura une valeur approchée (à 2−N près) de la forme : où b1 , b2 , ... , bN sont des entiers On écrira : x ≈ 0, b1 b2 . . . bN (on souligne pour préciser qu’on est en base 2). On parle de développement binaire / en base 2. H Exemple : Calculer le nombre x qui a pour développement binaire : 0, 11011. 5 2.2 Codage d’un réel quelconque Vous connaissez sans doute la notation scientifique d’un nombre décimal x ; par exemple : ; −423 = 12789 = ; 0, 0064001 = De manière générale, la notation scientifique de x est de la forme x = ε × m × 10e , où : • ε= • m s’appelle le signe (il donne le signe du réel x) ; s’appelle la mantisse ; • e s’appelle l’exposant. Une telle écriture existe toujours et est unique pour x non nul. De la même manière, en informatique, on utilise le fait qu’un réel x non nul s’écrit toujours de manière unique sous la forme : x = ε × m × 2e , où : • ε= • m • e s’appelle le signe (il donne le signe du réel x) ; s’appelle la mantisse ; s’appelle l’exposant. Remarque : L’écriture précédente s’appelle écriture en virgule flottante normalisée. Le terme « flottant » vient du fait que la place de la virgule dépend de l’exposant choisi. Le terme « normalisé » vient du choix d’une mantisse appartenant à [1, 2[. Coder un réel non nul dans un ordinateur revient donc à coder son signe, sa mantisse et son exposant. Avec la norme IEEE 754, on se donne 64 bits pour stocker ces trois données, dans l’ordre : • le codage du signe ne nécessite qu’un seul bit (en général, 0 code le signe positif et 1 le signe négatif) ; • le codage de l’exposant se fait sur 11 bits ; • le codage de la mantisse se fait sur 52 bits. H L’exposant : Il y a 11 bits pour représenter l’exposant. Si x est non nul et a pour exposant e compris entre −1022 et 1023 alors on prend comme code l’écriture binaire de e + 1023 qui est un entier entre 1 et 2046. H La mantisse : La mantisse m est nécessairement un nombre de l’intervalle [1, 2[. Sachant cela, il revient au même de coder m − 1 qui, lui, est dans [0, 1[. On utilise donc la méthode présentée plus haut pour coder m − 1. Avec 52 bits, cela permet donc d’avoir une précision de 2−52 . Remarque : Dans la plage de 11 bits dévolue à l’exposant, les entiers 0 . . . 0 = 0 et 1 . . . 1 = 2047 ne sont pas utilisés dans l’explication précédente. 6 L’exposant e égal à −1023 donc codé 0 . . . 0 = 0 avec une mantisse dont tous les bits sont nuls représente le nombre 0. Il y a deux façons de représenter 0 puisque le bit donnant le signe peut prendre les deux valeurs 0 ou 1 (les 63 autres bits sont à zéro). L’exposant décalé égal à 2047 est utilisé pour les situations particulières (+∞, −∞, d’autres choses plus compliquées : Nan - Not a number...). En mémoire, tous les bits dévolus à l’exposant sont égaux à 1. Exemple : Trouver le nombre x qui est représenté en flottant par : 1 1 0 0 0 0 0 0 0 0 1 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 ...que des 0 pour avoir au final 64 bits Rappelons que ceci n’est que la façon dont les choses sont codées ; lorsque le résultat d’un calcul en Python est un flottant, l’interpréteur ne nous renvoie pas ce code ni même la représentation ε.m.2e ; il écrit le nombre en écriture décimale ou bien avec la notation scientifique exponentielle : Python shell >>> -41 / 2 -20.5 >>> 1.5 * 2**167 2.8060831436753336e+50 7 3 Limites de la représentation des réels 3.1 Overflow et underflow Le problème le plus évident est celui du dépassement arithmétique, qui survient lorsque les 64 bits ne suffisent plus pour coder le réel x. Il y a dépassement ou overflow lorsque la mantisse et/ou l’exposant est "trop grand" 1 . Dans ce cas, l’ordinateur répondra +∞ ou −∞ selon le signe, ou renverra un message d’erreur : Python shell >>> 2.0**1023 8.98846567431158e+307 >>> 2.0**1024 Traceback (most recent call last): File "<pyshell#27>", line 1, in <module> 2.0**1024 OverflowError: (34, ’Result too large’) >>> sum(1/(2**k) for k in range(54))*2**1023 inf De même, si l’exposant ou la mantisse est "trop petite", il y a dépassement par valeurs inférieures, ou underflow. Python shell >>> 2.0**(-1100) 0.0 3.2 Inexactitude de la représentation, arrondis Un autre problème, même pour des nombres ni trop grands ni trop proches de 0, est que la représentation choisie est souvent non exacte. Pour -20,5, la représentation en mémoire est exacte, car le développement en base 2 de la mantisse 1 1 est finie. Mais pour certains nombres, le développement est infini : m=1+ + 4 32 1 1 1 1 1 1 1 1 1 + + + + + + + + + ... 2 24 25 28 29 212 213 216 217 i.e. : 1, 6 = 1, 10011001100110011... (ici le développement est périodique) 1, 6 = 1 + 1 1 = 0, 33333..., mais 0, 33333 n’est qu’une approximation de . 3 3 Lorsqu’il stocke le nombre 1, 6, l’ordinateur doit "tronquer" ou arrondir le développement infini, et on C’était déjà le cas en base 10 : on a n’a qu’une valeur approchée de 1,6. On constate que 1,6 est arrondi à : 1, 100110011001100110011001100110011001100110011001101 (la différence est à la toute fin !) 1. Plus précisément, quand l’exposant est strictement supérieur à 1023 (qui est le plus grand représentable sur 11 bits), ou s’il vaut 1023 mais que la mantisse est supérieure à la plus grande représentable sur 52 bits (que des 1). 8 En conséquence de ces arrondis, lorsque des calculs font intervenir ce nombre, on peut s’attendre à des erreurs : Python shell >>> 1.6 + 1.6 + 1.6 4.800000000000001 >>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> import math as m >>> (m.sqrt(10))**2 10.000000000000002 Ce n’est pas anodin lorsqu’on fait de la programmation : si on veut tester l’égalité de deux flottants et qu’il y a ainsi une légère différence, cela rendra un résultat erroné ! Python shell >>> 0.1 + 0.1 + 0.1 == 0.3 False >>> (m.sqrt(10))**2 == 10.0 False D’autres erreurs d’arrondis peuvent survenir lors de calculs, notamment entre des nombres dont les ordres de grandeur sont très différents. Observons : Python shell >>> 1 + 2**(-54) - 1 0.0 >>> 1 - 1 + 2**(-54) 5.551115123125783e-17 Explication : C’est dû au fait que l’ordinateur effectue les opérations dans l’ordre de lecture ! • Pour 1 + 2**(-54) - 1 : • Pour 1 - 1 + 2**(-54) : 9 Python shell >>> 2**(60) + 1 - 2**(60) 0.0 Explication : En conclusion, il faut retenir qu’il n’est pas possible, lorsqu’on travaille sur les flottants, de savoir de façon certaine si le résultat d’un calcul est égal à sa valeur "théorique". 3.3 Le problème de la comparaison à 0 Une des conséquences principales est qu’un test a == 0 n’a pas de sens si a est un flottant, puisse que celui-ci a pu souffrir d’erreurs d’arrondis. Illustrons ce problème avec des équations du second degré. Le programme suivant renvoie le nombre de solutions réelles d’une équation du second degré donnée par ses coefficients a, b, c : def nbsolutions(a,b,c): Delta = b**2 - 4*a*c if Delta == 0: nb = 1 elif Delta > 0: nb = 2 else: nb = 0 return(nb) Les trinômes x2 + 1.4x + 0.49 et x2 + 0.2x + 0.01 sont de discriminant nul, le programme devrait donc répondre 1. Cependant, lorsqu’on teste dans l’interpréteur : Python shell >>> nbsolutions(1, 1.4, 0.49) 0 >>> nbsolutions(1, 0.2, 0.01) 2 Commentaires : 10 Que suggérez-vous pour éviter ce problème ? 3.4 Quelques catastrophes dues à l’arithmétique flottante Il y a un petit nombre connu de catastrophes dans la vie réelle qui sont attribuables à une mauvaise gestion de l’arithmétique des ordinateurs (erreurs d’arrondis, d’annulation). Dans le premier exemple ci-dessous cela s’est payé en vies humaines. H Missile Patriot : En février 1991, pendant la Guerre du Golfe, une batterie américaine de missiles Patriot, à Dharan (Arabie Saoudite), a échoué dans l’interception d’un missile Scud irakien. Le Scud a frappé un baraquement de l’armée américaine et a tué 28 soldats. La commission d’enquête a conclu à un calcul incorrect du temps de parcours, dû à un problème d’arrondi. Les nombres étaient représentés en virgule fixe sur 24 bits, donc 24 chiffres binaires. Le temps était compté par l’horloge interne du système en 1/10 de seconde. Malheureusement, 1/10 n’a pas d’écriture finie dans le système binaire : 1/10 = 0,1 (dans le système décimal) = 0,0001100110011001100110011... (dans le système binaire). L’ordinateur de bord arrondissait 1/10 à 24 chiffres, d’où une petite erreur dans le décompte du temps pour chaque 1/10 de seconde. Au moment de l’attaque, la batterie de missile Patriot était allumée depuis environ 100 heures, ce qui avait entraîné une accumulation des erreurs d’arrondi de 0,34 s. Pendant ce temps, un missile Scud parcourt environ 500 m, ce qui explique que le Patriot soit passé à côté de sa cible. Ce qu’il aurait fallu faire c’était redémarrer régulièrement le système de guidage du missile. H Explosion d’Ariane 5 : Le 4 juin 1996, une fusée Ariane 5, à son premier lancement, a explosé 40 secondes après l’allumage. La fusée et son chargement avaient coûté 500 millions de dollars. La commission d’enquête a rendu son rapport au bout de deux semaines. Il s’agissait d’une erreur de programmation dans le système inertiel de référence. À un moment donné, un nombre codé en virgule flottante sur 64 bits (qui représentait la vitesse horizontale de la fusée par rapport à la plate-forme de tir) était converti en un entier sur 16 bits. Malheureusement, le nombre en question était plus grand que 32768 (overflow), le plus grand entier que l’on peut coder sur 16 bits, et la conversion a été incorrecte. H Bourse de Vancouver : Un autre exemple où les erreurs de calcul on conduit à une erreur notable est le cas de l’indice de la Bourse de Vancouver. En 1982, elle a créé un nouvel indice avec une valeur nominale de 1000. Après chaque transaction boursière, cet indice était recalculé et tronqué après le troisième chiffre décimal et, au bout de 22 mois, la valeur obtenue était 524,881, alors que la valeur correcte était 1098.811. Cette différence s’explique par le fait que toutes les erreurs d’arrondi étaient dans le même sens : l’opération de troncature diminuait à chaque fois la valeur de l’indice 11