INF4170 – Architecture des ordinateurs Notes de cours 1 Introduction L’architecture des ordinateurs a évolué depuis les années 50. Nous avons été témoins, au cours des dernières années, d’augmentations vertigineuses des vitesses et capacités des ordinateurs. L’introduction de machines à jeux d’instructions réduits formait la crête de la précédente vague de changements. Le développement des ordinateurs personnels, construits autour des microprocesseurs, fut le début de ce qui est maintenant considéré comme une révolution informatique. Certaines des avancées ont été transparentes pour les utilisateurs, d’autres servent d’arguments de marketing au point de s’intégrer à la culture populaire. Le développement du matériel n’a qu’un but, aller plus vite. Cet aspect, à lui seul, ne détermine pas la performance d’un ordinateur. Le logiciel qui s’exécute sur un ordinateur doit, idéalement, tirer partie d’une configuration matérielle dans le but d’être optimal. Le matériel doit donc, en contrepartie, répondre de manière souple et efficace, aux caractéristiques sémantiques des programmes, certaines de ces caractéristiques sont intrinsèques aux langages de programmation et systèmes d’exploitation. Les outils de programmation, les systèmes d’exploitation ainsi que la configuration mémoire des ordinateurs ont également évolué significativement depuis le début de l’informatique. Le développement concurrent et parallèle, tantôt en tête, tantôt à la traîne, des technologies de la programmation et du matériel, empêche de traiter d’architecture des ordinateurs en négligeant l’aspect logiciel. Cette association est particulièrement vraie au niveau des jeux d’instructions, et de l’organisation des composantes. Le cours présentera donc une synthèse des solutions matérielles aux besoins du logiciel. Il s’attardera aux problèmes, aux réponses apportées, leurs implications, et dans une certaine mesure leur implantation. Aujourd’hui, l’ordinateur est synonyme de numérisation de l’information et de manipulation numérique de celle-ci. Il existe pourtant deux types d’ordinateurs, l’analogique et le numérique. Les ordinateurs analogiques utilisent des phénomènes physiques (circuits électriques ou hydrauliques, règles, poids) qui simulent le modèle désiré, la lecture des tensions et pressions, positions et courants, de ces systèmes permet la résolution des équations. Ces ordinateurs (certains appelés intégrateurs ou différentiateurs) étaient d’usage courant jusqu’à l’arrivée de l’ordinateur numérique, plus simple d’utilisation mais possédant également leurs lots d’inconvénients. L’ordinateur numérique traite une information encodée, binaire, grâce à une série de bascules. La conversion d’un système analogique à un système numérique implique une phase d’échantillonnage où seules certaines valeurs seront conservées. La densité et la précision des échantillons établissent la qualité des données, elles influencent, mais sans être l’unique facteur, la qualité des résultats obtenus. Le passage inverse, du numérique à l’analogique est possible, l’approximation du système analogique l’est d’autant plus que la précision et la densité des valeurs numériques sont grandes (théorème de Shannon ou Nyquist). L’évolution de l’utilisation des ordinateurs, pour les applications de gestion et scientifiques, s’est accomplie en préservant la précision des données, des entiers pour les inventaires et les comptes, des nombres à virgule flottante ou fixe pour des valeurs décimales. On attribue généralement à John von Neumann l’invention de l’ordinateur numérique. Cette consécration est maintenant considérée comme surfaite. Il n’en demeure pas moins un acteur, et témoin privilégié, des balbutiements des ordinateurs de la première génération (1938-1953). Ces ordinateurs utilisaient des lampes à vide et des relais électromécaniques. Les premières machines ne stockaient pas de programmes en mémoire, elles devaient être programmées à l’aide de séries d’interrupteurs. L’ENIAC (1946), le EDVAC (1950), le EDSAC(1951) ainsi que la série des Mark I et Mark II effectuaient des calculs bit à bit et étaient bâtis de milliers de composantes. La vitesse se mesurait en microsecondes et la chaleur dissipée par chaque composante se mesurait en watts plutôt qu’en milliwatts comme pour celles d’aujourd’hui. Le premier ordinateur commercial, le UNIVAC fut introduit en 1951. L’invention du transistor en 1948 permit une première miniaturisation des ordinateurs, bien que cette transition ne fut complétée qu’au début des années 60. Les transistors remplacent avantageusement les lampes à vide, et relais électromécanique, puisqu’ils occupent un espace restreint, consomment et dissipent peu d’énergie tout en étant plus résilients. Cette seconde génération (1952-1963) voit également l’émergence de nouveaux outils, l’assembleur inventé par Wilkes (1951) permit de développer plus aisément les programmes, le FORTRAN (1956) par IBM, le COBOL (1959) par l’armée américaine ainsi que Algol en 1960, établirent les fondements de l’informatique et rendirent plus accessible l’utilisation des ordinateurs. Les mémoires étaient bâties autour de pièces magnétisées, les tores, dont l’orientation est modifiée par le passage d’un courant électrique. Ces mémoires, complexes à construire, avaient un temps d’accès (1 s), un mégaoctet valait 1 million de dollars. Les coûts d’opération des ordinateurs étant prohibitifs, la rentabilité des systèmes passait par le développement de techniques qui alliaient le travail du processeur et les entrées/sorties. L’objectif de diminuer le temps pris par un programme pour s’exécuter, se réalisa dans le cadre des systèmes d’exploitation, il demanda de nouveaux supports par le matériel. L’introduction des disques à accès nonséquentiels, en 1963 par IBM, révolutionna à son tour le monde de l’informatique. Les apprentissages de la décennie précédente, tant dans le domaine de la technique que dans le domaine théorique, servirent de bases pour une nouvelle génération de machines (1962-1975). La foi dans les capacités techniques, se heurtant aux limites des connaissances, se traduisit par des machines extrêmement complexes qui devaient résoudre tous les problèmes et se gérer elles-mêmes. La stabilité appréhendée des architectures de cette génération rendit possible la notion de familles d’ordinateurs, le principe sous-jacent étant de fournir une version d’une architecture à l’échelle de la taille des problèmes à résoudre et de l’organisation acquérant celle-ci. La miniaturisation des composantes de cette génération se fit en utilisant des circuits intégrés, inventés à la fin des années 50. Les premiers circuits intégrés sont nommés circuits à petite échelle d'intégration (SSI small scale integrated). La densité des composantes augmenta de manière fulgurante au cours des années 60, en moyenne, elle doublait chaque année. Cette progression diminua dans les années 70, elle quadrupla aux trois ans. Ces croissances de performance se firent dans un contexte où le prix des composantes demeurait constant, il s’en suivi d'une baisse vertigineuse du prix des éléments de base des machines. La miniaturisation des composantes implique que la distance à parcourir entre deux points est moindre, d’où une augmentation de vitesse mais également une diminution de puissance et des besoins en refroidissement. Le nombre moins élevé de connexions entre les éléments diminue le nombre de pannes dues aux bris. La technologie des circuits associée à la fin de cette période est appelée intégration moyenne (MSI medium scale integrated). La quatrième génération d’ordinateurs (1972-1985) se développa autour d’un nouveau niveau dans les circuits intégrés, la haute intégration (LSI large scale integration), possédant un nombre de composantes de l’ordre de 1000 par élément. La mémoire virtuelle, introduite par IBM au début des années 70, permit d’augmenter l’espace adressable des programmes et par conséquent, la taille des programmes. Notez qu’il existe des ordinateurs dont la taille de la mémoire physique est supérieure à l’espace adressable. Le prix par bit de la mémoire bâtie autour de circuits intégrés descendit sous celui du bit de la mémoire magnétique vers 1974, permettant la mise au rancart des mémoires à tores. L’introduction des microprocesseurs, Intel introduit le 4004 en 1971, et un des premiers micro-ordinateurs dans la seconde moitié des années 70, ouvrirent la voie à une révolution dans le monde de l’informatique. Parallèlement à ces développements, plusieurs super ordinateurs furent introduits et le traitement parallèle fit un bond. Certaines machines traitant simultanément plusieurs données, en exécutant la même instruction sur des éléments de calcul différents, alors que d’autres traitaient des données et des instructions différentes. La complexité d’exécution des instructions, les faibles fréquences d’utilisation des instructions ainsi que de certains modes d’adressage, provoqua dans la seconde moitié des années 80 une suite d’études sur la possibilité de réduire la complexité des instructions. L’hypothèse étant que des outils de développement pouvaient être assez puissants pour générer des suites d’instructions simples à exécuter sur des machines moins complexes. Les avancées de la miniaturisation à très grande échelle (VLSI very large scale integrated), aidant à améliorer la vitesse, et l’utilisation de pipelines, qui traitent en parallèle différentes instructions rendues à des étapes intermédiaires différentes de leur cycle d’exécution, permettent de diminuer le taux apparent de cycles nécessaires pour exécuter les programmes, d’où de meilleures performances. Ces machines sont appelées sont appelées RISC, pour Reduced Instruction Set Computers, par opposition au CISC, Complex Instruction Set Computer. Le développement de ces machines constitue la cinquième génération et nous y sommes toujours. La contrainte ultime des machines est la lumière, cette contrainte existe sous plusieurs formes, elle limite la vitesse de propagation du courant électrique (95% de la vitesse de la lumière), elle limite également le procédé de fabrication des circuits. Les circuits intégrés sont "gravés" à l’aide de la lumière. Ils sont constitués de couches superposées d’isolant et de conducteur. On enduit un morceau de silicium d’une couche d’oxyde de métal, puis d’une émulsion photosensible, on recouvre le tout d’un masque laissant le tracé du circuit désiré à découvert, on expose alors le masque à la lumière et l’émulsion est dégénérée, on cuit l’émulsion qui sert à son tour de masque pour un traitement à l’acide qui grave le métal, l’émulsion est par la suite lavée. La surface gravée est remplie d'un conducteur. La seconde couche est appliquée au-dessus de la première et ainsi de suite. On atteint actuellement les limites de l’utilisation de la lumière parce que la largeur des gravures approche la longueur d’onde de la lumière. On travaille à diminuer la largeur mais cela occasionne plusieurs problèmes. Des techniques sont mises au point pour utiliser des rayons X et des faisceaux d’électrons. Les caractéristiques recherchées qui assurent, du moins jusqu’à aujourd’hui, le succès d’un ordinateur sont : la généralité, soit la capacité d’exécuter toute une gamme d’applications; l’efficacité, soit la capacité de générer une quantité de résultats dans un certain laps de temps; et la malléabilité, soit la possibilité d’avoir plusieurs configurations différentes (famille d’ordinateurs). La disponibilité en qualité et quantité, l’ouverture du système et sa simplicité, les possibilités de mises à jour et de remplacement de pièces sont autant de facteurs qui affectent également le succès d’un ordinateur. 2 Ordinateur simple 2.1 Architecture, implantation et réalisation L'homme utilise différents niveaux d'abstraction pour simplifier et décomposer, un objet complexe en plusieurs couches. Dans le cas du matériel, on distingue trois niveaux: l'architecture, l'implantation et la réalisation. L'architecture décrit l'aspect présenté à l'utilisateur de la machine. Elle comprend l'assembleur, les tailles et types de données, le nombre de registres et leurs noms, les modes d'adressage, les contraintes de programmation. L'implantation décrit le choix des composantes et leur organisation logique. Par exemple, l'utilisation de bus parallèle ou série, la taille du cache, la position des unités fonctionnelles sur le chip. La réalisation représente le matériel: filage, portes et circuits. 2.2 Modèle de von Newmann Le modèle de von Newmann propose de stocker indifféremment les instructions et les données d’un programme en mémoire de l’ordinateur. La généralité de la mémoire étant gage de souplesse, chaque case possède sa propre adresse. La distinction entre instruction et donnée est assurée par un compteur ordinal, intégré à l’unité de traitement, passant séquentiellement d’une instruction à une autre. L’unité de traitement contrôle l’accès à la mémoire et aux entrées/sorties. Le lien unissant la mémoire et l'unité de traitement est un goulot d'étranglement parce qu'il sert au transfert, tant dans un sens que dans l'autre, des adresses (vers la mémoire) et des données et instructions (vers l'unité de traitement). Facteur aggravant, les vitesses de ces composantes sont différentes (1 ordre de grandeur), contraignant également la performance du système. Une variante de ce modèle, appelé architecture de Harvard, sépare la mémoire de données des instructions. Elle est fréquemment retrouvée dans les caches parce que le comportement des références aux instructions est différent de celui des accès aux données. Cette variante n'est cependant pas à l'abri de problèmes, que nous verrons plus loin dans la section sur les mémoires. 2.3 Composantes et cycle des instructions La section précédente décrit une machine simple. Ses principales composantes sont la mémoire, les périphériques et l'unité centrale de traitement. Cette dernière est constituée de l'unité arithmétique et logique (UAL) et de l'unité de contrôle. Le liens physiques unissant les composantes sont appelés bus. Ils peuvent être aussi bien utilisés pour contrôler les composantes que transmettre des données et instructions. Pour exécuter un programme, l’ordinateur traite une série d’instructions. Dans le but d’exécuter une instruction, les étapes à suivre sont la localisation de l’instruction à exécuter, son transfert vers l’unité centrale de traitement, son décodage, la lecture de ses opérandes, le calcul ou traitement, l’écriture des résultats et le calcul de la prochaine valeur du compteur ordinal. Le passage d’une opération à la suivante est provoqué par le battage de mesure réalisé par l’horloge du système, à chaque pulsation correspond l’exécution d’une partie d’une opération. C'est l'unité de contrôle qui contient, pour chaque instruction, toutes les parties à réaliser. Le stockage du contenu de la mémoire entre deux parties consécutives est assuré par des registres temporaires. Ceux-ci agissent comme des interfaces entre deux pièces du processeur. C'est le propre des programmes que de choisir entre plusieurs cas en calculant et comparant des expressions à l'aide de l'ULA. L'exécution d'une instruction par l'UAL produit deux résultats. Le premier, arithmétique ou logique à proprement parler, va être stocké en mémoire par la prochaine étape du cycle de l'instruction. Le second résultat est transmis au registre d'état associé à l'unité de contrôle. Celui-ci indique les débordements, retenues, emprunts, signe ou égalité avec zéro du résultat principal. Il participe donc, dans le cas des branchements, au calcul de la prochaine valeur du compteur ordinal. Tel que décrit, le cycle des instructions permet d'exécuter tous les programmes. Cette image est trompeuse, puisqu'elle implique une boucle sans fin où l'ordinateur ne peut répondre à un événement extérieur. A cet effet, le cycle des instructions supporte les interruptions. Cette capacité permet d'assurer un temps de réponse acceptable et à point pour, par exemple, traiter une division par zéro, un défaut de page, un caractère tapé au clavier, etc... Suite à l'interruption, le cycle de l'instruction peut être repris, au début de l'instruction ou, au point où s'est produite l'interruption. Cette reprise dépend des actions produites par l'instruction sur l'état de la machine. 2.4 Niveaux matériels et logiciels En introduction, on a mentionné qu'un ordinateur est composé de matériel et logiciel. Ce mélange est stratifié, il se décompose en trois niveaux, le niveau application (les programmes usagers), le niveau système (compilateurs, éditeurs de liens, système d'exploitation, bibliothèques partagées) et le niveau matériel (sa couche supérieure faite des séquences d'opérations, contenues par l'unité de contrôle, et sa couche inférieure, la circuiterie). La souplesse, c'est-à-dire la capacité d'être modifié diminue du niveau application vers le niveau matériel, alors que les coûts de développement sont inversés. Par exemple, on estime à 350 personnes/année le développement du Pentium III, le développement d'un logiciel qui a la même durée de vie peut être de 30 personnes/années. Les coûts prohibitifs du développement du matériel alliés aux besoins des applications (compatibilité entre plate-formes, interfaces, réutilisation du code, simplicité d'installation et standardisation) ont eu pour effet d'imposer au niveau intermédiaire des contraintes. Un effort particulier est fait pour développer un niveau système polyvalent et performant. Cet effort est également astreint à remplir une condition marketing de l'informatique, le développement d'applications doit être simple et est entrevu par l'industrie comme devant être à la portée de tous. Ces conditions sont vues comme garantes de la pénétration dans le quotidien des gens. 2.5 Evolution des modèles d'architecture Il existe plusieurs architectures, on note toutefois une évolution. Celle-ci est particulièrement visible lorsque l'on s'attarde aux jeux d'instructions. Le modèle le plus simple est sans contredits, l'architecture à pile. Cette architecture se prête à merveille à une évaluation post-fixe des expressions. Cette architecture de base est encore souvent utilisée comme modèle de référence pour exprimer des programmes. Les compilateurs extraient de cette représentation simple une version de code optimisée pour une machine plus complexe. Cette architecture est encore à la base du traitement des nombres à virgule flottante pour les architectures Intel. Le processeur n'a besoin que de deux registres internes, le pointeur de pile et le compteur ordinal pour exécuter des programmes avec cette architecture (les opérandes étant sur le sommet de pile, le résultat principal de l'UAL remplace les opérandes). La seconde architecture est la machine à accumulateur. Elle répond efficacement aux expressions A := A + B. L'accumulateur étant à la fois opérande source et destination, il libère un accès mémoire. L'accumulateur est un registre dédié. Si on attribue une valeur de 100 à un accès mémoire et 10 à un accès registre, l'architecture à pile requiert une valeur de 300 pour une opération UAL alors que celle-ci requiert une valeur de 120, soit un gain appréciable en vitesse. Le i8080, la famille des micro-contrôleurs 6800 et les 6502 sont de bons représentants de ce modèle. La troisième architecture est une généralisation de l'architecture à accumulateur. On étend à plusieurs registres la possibilité d'être source et destination, tout en maintenant le second opérande en mémoire principale ou registre. On parle alors d'architecture à deux opérandes. Cette évolution n'est pas sans problèmes puisqu'elle impose l'encodage explicite de tous les opérandes, donc la taille des instructions augmente. On appelle densité de code la taille nécessaire pour encoder les instructions, plus le nombre de bits est élevé, avec possiblement des trous, plus la densité du code est réduite. Les familles Motorola 68000 et Intel x86 sont des représentants de ce modèle. En spécifiant différents registres pour les destinations et sources, on introduit un nouveau modèle, l'architecture à trois opérandes. Cette architecture n'est utilisée que pour des machines RISC. Une nouvelle vague d'architectures a fait son apparition. Le nombre de sources et de destinations n'est plus le fait saillant de celles-ci. Il s'agit plutôt de grouper plusieurs instructions de manière à donner plus de latitude au processeur pour exécuter en parallèle des instructions dans des unités super-scalaires. Le parallélisme est géré soit par le compilateur en le rendant explicite (machines EPIC) ou laissé à la détermination par le matériel (machines VLIW). 3 Mesures de performance L'horloge bat la mesure d'une architecture. L'unité de mesure associée à l'horloge est le Hertz (Hz), 1 Hz représente un cycle par seconde. La période d'un cycle est mesurée en secondes, elle est l'inverse de la fréquence. Idéalement, une cadence rapide devrait être garante de l'efficacité des machines. Cependant, le cycle des instructions étant composé de plusieurs étapes, le nombre de cycles nécessaire à l'exécution des instructions varie d'une machine à l'autre. 3.1 Loi de Amdahl La loi de Amdahl décrit l'impact temporel de l'accroissement de performance d'un système: t f (t / F) (ti t) où tf représente le temps après modification, t le temps associé à la partie du procédé qui est accéléré, F le facteur d'accélération, ti le temps initial. Son application aux architectures se base sur le partage de certaines étapes du cycle d'exécution ainsi que la répartition des fréquences d'utilisation des instructions. Cette dernière varie beaucoup d'une classe à l'autre. Par exemple, les opérations de copie occupent le premier rang avec près de 60 % des cas. En rendant plus rapides certaines étapes et instructions parmi les plus fréquentes, la somme des gains devient supérieure à celle de l'optimisation des cas les moins fréquents. Cette règle est générique et la portée de son application dépasse le matériel et le logiciel. 3.2 Temps d'exécution Le temps d'exécution d'un programme est égal au nombre de cycles du programme multiplié par la période d'un cycle. Cette sommation est exacte et propose, en première analyse, un métrique qui semble intéressant. Il donne à l'usager une idée précise pour le programme. Il ne permet pas, cependant, de comparer deux architectures, ni même deux programmes sur une même architecture. La comparaison de deux architectures requiert la prise en compte de toutes les particularités des architectures. Pour couvrir celles-ci, l'équation du temps d'exécution est modifiée pour utiliser la moyenne du nombre de cycles par instruction, CPI. On établi donc le temps machine comme étant le produit du CPI par le nombre d'instructions. Le nombre d'instructions utilisé par un programme n'est pas significatif puisqu'il est possible de sélectionner différentes séquences d'instructions pour effectuer le même calcul. Il est par ailleurs possible d'obtenir une séquence plus courte nécessitant plus de cycles et une séquence plus longue avec moins de cycles. 3.3 MIPS et MFLOPS Le MIPS représente un million d'instructions par seconde. Ce métrique en soit n'est pas significatif puisque les architectures n'utilisent pas les mêmes instructions. Deux compilateurs sur une architecture donnée ne génèrent pas des séquences de code identiques d'où des temps et nombres d'instructions différents. Le MFLOPS représente un million d'opérations à virgule flottante par seconde. Ce métrique n'est pas significatif étant donné les disparités des jeux d'instructions pour les nombres à virgule flottante. Certaines architectures fournissent des opérations complexes sous forme d'instructions alors que d'autres n'en ont pas ou sont différentes. 3.4 Suites de performance Pour décrire et comparer des architectures et systèmes différents, des suites de programmes d'utilisation courante sur ces machines sont utilisées. Puisqu'ils représentent une utilisation usuelle des machines, ils ont tendance à être représentatifs des performances. Les plus populaires sont les BYTEMARK du défunt magazine Byte pour les micro-ordinateurs et les SPECMARK pour les stations de travail. Il existe plusieurs suites de validation et performance sur le marché pour évaluer les compilateurs. Tous les compilateurs commerciaux (et également certaines versions de gcc) sont optimisés pour avoir de bonnes notes et les programmes de test sont souvent non représentatifs des conditions réelles d'utilisation. Il n'en demeure pas moins que les performances enregistrées avec des logiciels et de opérations d'usage courant sont les meilleurs métriques pour comparer deux systèmes. 4 Données et instructions L'unité adressable de base est l'octet formé de 8 bits. Pour représenter des valeurs, on utilise une suite d'octets, le nombre de valeurs est 2 bits. Généralement, on appelle mot un groupe d'octets qui supporte les valeurs entières de base de l'architecture, ce nombre de bits est généralement égal à la largeur du bus de données de la machine. Les mots ont généralement 2 ou 4 octets. Il est également usuel d'utiliser les termes : double mot et quadruple mot pour représenter les autres valeurs. La numérotation des octets dans un mot est à l'origine de plusieurs incompatibilités entre des machines. Le terme consacré est le "endian". Cette dénomination provient du conte "Le voyage de Gulliver". Au cours de son périple, il fait naufrage sur une île où les habitants d'une partie sont en guerre avec les habitants de l'autre. L'origine du conflit est une divergence sur la méthode à utiliser pour manger un oeuf à la coque. Les premiers commencent par le bout pointu, les seconds par le bout rond. Par référence au bout pointu on appelle "little endian" les suites d'octets dont la numérotation attribue la valeur minimale à l'octet qui se trouve à l'adresse la plus basse alors que, par opposition, on appelle "big endian" les suites dont le numéro maximal est associé à l'adresse la plus basse. Pour passer de l'un à l'autre, les octets qui forment les mots doivent être renversés (l'ordre des bits dans un mot n'est pas changé). 4.1 Valeurs entières et à virgule flottante L'encodage des entiers utilise le complément à deux. Cet encodage utilise le bit le plus significatif pour représenter le signe d'une valeur. Les valeurs négatives sont calculées en additionnant un au complément logique du nombre. Il est pratique puisqu'il permet de n'avoir qu'un seul encodage pour la valeur 0 et l'inspection visuelle indique clairement une valeur négative. Nous verrons plus tard que l'implantation de la soustraction est très simple en utilisant le complément à deux. Les opérations arithmétiques génèrent des débordements si le signe du résultat est différent du signe des opérandes (seules deux opérandes de même signe peuvent causer des débordements en addition et deux opérandes de signes opposés en soustraction). Il y a retenue si l'opération génère un bit supplémentaire. Notez qu'il peut y avoir débordement sans avoir de retenue et retenue sans débordement. FFFF + 0001 1 0000 Dans cet exemple, sur 16 bits, la valeur 1 est additionnée à la valeur -1, le processeur n'a pas "conscience" de la taille des opérandes, pour lui le patron 0xFFFF pourrait aussi bien être la partie basse d'une valeur sur 32 bits, il fait donc l'addition bit à bit et génère un 1 qui se retrouve dans le bit de retenue, la somme étant nulle, le bit zéro doit être levé et par conséquent le bit négatif ne l'est pas. Puisque les deux opérandes, traitées sur 16 bits, sont de signes opposés, il n'y a pas de débordement. Il existe un autre encodage, le BCD (binary coded decimal) qui encode les valeurs de 0 à 9 sur les 4 bits inférieurs d'un octet (4 bits sont appelés nibble). Le nombre prend donc une suite d'octets proportionnelle à la taille des valeurs à encoder. Cette notation est utile puisqu'elle permet aisément de convertir ses valeurs en codes de caractères imprimables. On n'a qu'à introduire un préfixe dans la partie haute des octets. Les architectures supportent des opérations PACK et UNPACK pour concentrer valeurs. L'encodage des nombres à virgule flottante se compose de plusieurs parties : un exposant, une mantisse et un signe. Puisque les nombres décimaux peuvent être représentés avec des exposants différents, les calculs doivent être faits sur des valeurs normalisées de manière à réduire les conversions et pertes de précision. Il existe plusieurs normes qui régissent la manipulation des virgules flottantes. Plusieurs sont introduites par les manufacturiers, la norme internationale, depuis 1985, est la IEEE-754. Les normes spécifient les plages de validité, les formats, les comportements lors des calculs. On utilise la formule suivante, en IEEE 754, pour représenter les nombres à virgule flottante: 1signe 1.mantisse 2 (exposant biais) . Un nombre est normalisé lorsque (en binaire), le 1 est placé directement à gauche de la virgule, la mantisse consiste donc dans la partie de droite. Puisque le bit le plus significatif représente le signe, le patron de bits de l'exposant possède également un bit de signe. Les exposants négatifs représentent des nombres très petits (inférieurs à 1) mais semblent alors supérieurs aux exposants positifs, on introduit un biais pour faire une translation qui fasse paraître les exposants négatifs plus petits que les exposants positifs. On nomme "underflow" le débordement inférieur lorsqu'une valeur est trop petite pour être représentée en format normalisé. Un débordement supérieur se produit lorsque la valeur est trop grande pour être représentée en format normalisé. Des patrons de bits particuliers sont associés aux valeurs zéro (s=0, m=0, e=0), NaN (not a number s), infini (s=0, e=-1, m=0), - infini (s=1, e=-1, m=0). 4.2 Instructions et adressage Les instructions contiennent plusieurs champs qui encodent l'opération à exécuter, la taille des opérandes, le type et la location des opérandes. La complexité de l'encodage nécessaire pour les instructions implique l'utilisation de plusieurs mots mémoire. Afin d'améliorer la densité du code, l'encodage est généralement de taille variable et comporte un mot de base. Les différentes architectures, telles les machines à pile et accumulateur ont souvent des opérandes implicites qui permettent de sauver des bits d'encodage. On nomme modes d'adressage les méthodes d'accès aux opérandes. Le calcul de l'opérande peut être statique, c'est-à-dire que sa position est fixée lors de la construction du programme, ou bien dynamique, la position étant calculée lors de l'exécution du programme. Cette dernière méthode utilise des combinaisons d'adresses de base et de déplacements fixes ou variables. Ces modes s'adaptent naturellement aux structures de données élémentaires, enregistrements et tableaux. Dans le cas de tableaux l'adresse d'un élément est déterminée par : Base (i borneinf ) * taille(élément) . Il existe des modes d'adressage plus complexes qui permettent de "sauver" des instructions. Par exemple, pour parcourir un tableau il est souvent souhaitable d'avoir l'opérande qui pointe vers une source ou destination se déplacer vers le prochain élément, cette incrémentation, positive ou négative, peut être faite avant ou après l'accès à la mémoire, on parle respectivement de pré et post incrément (décrément). Dans le cas des branchements il est souvent utile de pouvoir calculer la destination par rapport à la position courante du compteur ordinal, on parle alors d'adresse relative. Il existe une trentaine de modes d'adressage différents qui ont été implantés, il est cependant rare d'en voir plus d'une dizaine sur une architecture. La table qui suit dresse la liste des modes de base les plus courants. 1. Adressage direct: l'instruction contient l'adresse de l'opérande Op L X86 68K SPARC PowerPC Opérande mov and.l call bl ax, Data 0xd9cd, d7 routine routine où Data est une étiquette n'existe pas pour l'arithmétique n'existe pas pour l'arithmétique 2. Adressage registre: l'instruction contient le numéro du registre qui contient l'opérande Op L X86 68K SPARC PowerPC Reg inc add.w add and eax d6,d7 %i1, %i3, %i1 r31,r31,r30 3. Adressage immédiat: l'opérande est imbriquée dans l'instruction Op L Imm X86 68K SPARC PowerPC cmp addq.w add andi ebx, 32 d6, 6 %i1, %i1, 1 r31,r31,0 4. Adressage registre-indirect: l'instruction contient un registre qui contient l'adresse de l'opérande Op L Reg X86 68K SPARC PowerPC Opérande call jmp ret blr [eax] [A0] %i7 r12 5. Adressage base-déplacement: l'instruction contient un registre de base et un déplacement par rapport à cette base. Op L Reg Opérande Depl Base + X86 68K SPARC PowerPC mov add.l ld lbz eax,[ebp+8] d0,8[a6] %i1,[%g2+32] r30,12(r3) 6. Adressage base-déplacement-index: l'instruction contient un registre de base, un registre d'index et possiblement un déplacement. Op L Reg Idx Opérande Depl + X86 68K SPARC PowerPC mov move.l illégal lbzx Base es:[si+$1234], $5678 d0, 8[a1, d0.w] r3, r4, r5 7. Adressage relatif: l'instruction contient un déplacement qui doit être additionné au compteur ordinal pour calculer l'étiquette de branchement. Op L X86 68K SPARC PowerPC 4.3 Rel PC = PC + Rel call bne b b $+32 $+32 $+32 $+32 Modèles d’exécution Les programmes sont constitués de code et de data. La disposition des différentes zones de code et data détermine des modèles d'exécution sur lesquels reposent les systèmes. Le modèle le plus simple est un ensemble entrelacé de code et data. Ce modèle est utilisé pour des applications simples. Il est également utilisé pour les programmes COBOL, langage qui ne supporte pas les variables locales. Un modèle plus évolué supportant les routines ayant des variables locales est utilisé par FORTRAN, les variables locales sont réutilisées par chaque invocation de routine. Le support à la récursivité exige de créer des contextes qui sont associés à chacune des invocations des routines. La manière choisie pour implanter ce modèle est une pile. Aujourd'hui il est courant de voir les programmes divisés en quatre sections, celles-ci s'adaptent à tous les modèles. La mémoire est divisée en data (constantes, variables initialisées et non-initialisées), code exécutable, pile (stack) et un espace supportant l'allocation dynamique (heap). Pile Monceau Code Data 4.4 Support aux langages Les langages de programmation utilisent les ressources architecturales pour implanter leur sémantique. L'inverse peut sembler également vrai, pour des raisons évidentes, le succès des architectures devrait être lié à l'efficacité de leur support aux langages de programmation. Ceci n'est cependant pas exact puisque les architectures sont soumises à plusieurs autres facteurs qui bien souvent prennent le pas. L’un des plus importants facteurs influençant la performance est l'ensemble des conventions d'appel et retour de routines, ces conventions incluent le passage de paramètres. La sémantique des langages spécifie les ordres d'évaluation et de passage de paramètres, mais bien souvent la spécification du langage laisse aux fabricants différents choix d’implantation permettant d’optimiser la génération de code. De manière générale le protocole d’entrée comporte les étapes suivantes : Passage des paramètres Sauvegarde de l'adresse de retour et branchement Sauvegarde du contexte de la routine appelante Création du contexte de la routine appelée Le protocole de sortie quand à lui: Sauvegarde de la valeur de retour Elimination du contexte de la routine appelée Rétablissement du contexte de la routine appelante Récupération de l'adresse de retour Elimination des paramètres et retour Dans le cas du langage C, on impute complètement la responsabilité aux procédures appelantes pour le passage et l'élimination des paramètres ainsi que l’évaluation de gauche à droite des arguments. Les langages du style Pascal ou Modula permettent à la routine appelée de manipuler les paramètres, ce passage est plus efficace puisque les routines sont libres de copier ou non les paramètres non scalaires passés par valeur. Certaines architectures implantent des instructions spécifiques pour manipuler les contextes (LINK et UNLK, MOVEM pour le MC680x0 par exemple). D'autres exigent des compilateurs la création de cadres fixes qui englobent, au temps de compilation, la taille maximale qui sera occupée par les arguments des appels que la routine fera. Par exemple, le SPARC utilise une zone de 64 longs directement au dessus du pointeur de pile pour sauvegarder le contenu d'une fenêtre de registre en cas de débordement, cet espace doit toujours être libre d'où l'impossibilité de modifier la taille d'un contexte de façon dynamique. Par contre, dans certains cas comme le PowerPC, c'est une contrainte du système d'exploitation qui demande de réserver un espace sur la pile (226 octets, appelé zone rouge). Paramètres passé par l'appelant Adresse de retour FP + déplacement Ancien pointeur de cadre Sauvegarde Variables locales et temporaires FP FP - déplacement SP Il est usuel pour plusieurs architectures de fournir des registres dédiés aux pointeurs de pile et de cadre. Le mode d'adressage en base-déplacement est normalement utilisé, un déplacement positif pour les paramètres et un négatif pour les variables locales et temporaires. La programmation orientée-objet utilise généralement des tables de pointeurs vers des routines pour représenter les méthodes. Les appels requièrent souvent des recherches dans les hiérarchies de tables pour effectuer un appel, que les appels soient faits à l'aide d'une routine de recherche spécialisée (dispatcher) ou un bout de code inséré spécifique à chaque site d'appel (thunk), la sélection de l'adresse de branchement se fait de manière dynamique, il est donc nécessaire d'avoir un mode d'adressage indirect pour les instructions d'appel de routine. Ce même adressage peut également être utilisé pour faire un branchement (sans retour) dans le cas d'une énoncé switch ou case par exemple. L'amélioration de la performance d'exécution des branchements passe par l'utilisation d'un comportement privilégié à la prise ou non de ceux-ci. En fonction de ces règles, le processeur commence le transfert d'instructions. L'assomption sous-jacente étant que les boucles sont écrites pour être exécutées plusieurs fois et que les conditions les plus usitées sont insérées les premières dans les conditionnelles en cascades. On nomme cette caractéristique : prédiction. Lorsque les règles sont fixes, on parle de prédiction statique, le compilateur, ou le programmeur, utilise alors un choix judicieux de négations des expressions conditionnelles pour maximiser la performance. L'implantation de la prédiction dynamique impose au processeur de conserver des statistiques de branchement pris ou non, dans une table de paires <adresse, condition>. Elle est principalement implantée dans les nouvelles architectures. 4.5 Support aux systèmes d’exploitations Il existe plusieurs liens entre le système d'exploitation et l'architecture d'un ordinateur. L'atomicité de certaines instructions qui permettent l'accès exclusif de la mémoire permet l'exclusion mutuelle. Le support à la mémoire virtuelle impose l'implantation de mécanismes de pagination et de segmentation. Ces mécaniques sont transparentes aux utilisateurs mais possèdent des interfaces système servant à leurs gestions. 4.6 Formats d’exécutables et de fichiers objet Il existe plusieurs formats de fichiers. Les composantes principales sont des entêtes décrivant les parties des fichiers, des sections de code et données ainsi que des entrées utilisées par le chargeur pour terminer l'édition de liens nécessaire à la mise en marche du programme. Les formats les plus populaires sont le COFF (Common Object File Format) développé pour UNIX et dont il existe plusieurs variantes (MS-COFF Microsoft, UCOFF MIPS, SGI et Digital, XCOFF IBM, PEF Apple), et ELF utilisé sous UNIX et Linux. Ce dernier format est de plus en plus courant dans les systèmes embarqués. La majorité des formats permettent d'attribuer les sections (lecture ou lecture/ecriture, adresses préférées), celles-ci peuvent alors être plus aisément partagées entre les programmes. Le format binaire le plus simple est l'image pure qui contient tout le code et les données aux "bonnes" adresses et qui ne requiert pas d'édition lors du chargement du programme. Les fichiers exécutables sont généralement accompagnés de fichiers de symboles servant au déboguage. Ces fichiers contiennent une représentation des types utilisés par le programmes, une table de routines qui contient les références vers les fichiers et les paires <compteur ordinal, numéro de ligne>, les variables locales et globales. Le déboggeur se construit une image du programme lors du chargement en utilisant ces tables et permet ainsi à l'usager de travailler avec des symboles plutôt que des adresses et des instructions machines. 4.7 Bibliothèques partagées et PIC La mise en oeuvre de l'édition de liens au début de l'exécution ou lorsqu'une partie du programme est déplacée en mémoire impose une perte de performance. Cette dégradation de performance peut être compensée en utilisant des références relatives pour les appels, de cette manière il n'est pas nécessaire de relocaliser les adresses des routines. On appelle cette méthode PIC pour Position Independent Code. La généralisation de cette méthode aux bibliothèques partagées demande alors de créer une table de contenu pour chaque routine externe. De cette manière, les appels locaux sont relatifs et les appels externes sont indirects au travers de cette table. Pour les références du code vers les données, le problème est un peu plus complexe puisque l'on doit prendre les adresses (pointeurs), la méthode utilisée consiste à représenter une adresse comme la somme de l'adresse de la routine et du déplacement de la donnée par rapport à celle-ci, au début des routines on ajoute au prologue des instructions qui extraient la valeur courante du compteur ordinal, celle-ci est ensuite additionnée au déplacement. Comme il est impossible de distinguer entre les données de la bibliothèque et les données d'une autre, le data global est toujours accédé en double indirection, la somme calculée est un pointeur vers un pointeur, ces pointeurs font partie de la même table d'indirections que les routines externes. On appelle souvent cette table GOT pour Global Offset Table. Ainsi le code peut être partagé par plusieurs programmes. Seules les GOT et le data sont spécifiques à chaque programme.