Fonctionnement d'un ordinateur/Les jeux d'instructions
Les instructions d'un processeur dépendent fortement du processeur utilisé. Le jeu d'instructions d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Mais les architectures x86 et Power PC ne sont pas les seules au monde : il existe d'autres types d'architectures qui sont très utilisées dans le monde de l’informatique embarquée et dans tout ce qui est tablettes et téléphones portables derniers cris. On peut citer notamment les architectures ARM, MIPS et SPARC. Pour résumer, il existe différents jeux d'instructions, que l'on peut classer suivant divers critères.
Les jeux d'instruction RISC vs CISC
[modifier | modifier le wikicode]Dans le chapitre précédent, nous avons vu comment sont encodées les instructions, ainsi que de nombreux paramètres : les modes d'adressage supportés, leur encodage, la taille des instructions, l'encodage de l'opcode, etc. Et tous ces paramètres sont suffisant pour parler des processeurs RISC et CISC. Il s'agit d'une classification qui sépare les processeurs en deux catégories :
- les RISC (reduced instruction set computer), au jeu d'instruction simple ;
- et les CISC (complex instruction set computer), qui ont un jeu d'instruction étoffé.
Reste à expliquer ce que l'on veut dire quand on parler de jeu d'instruction "simple" ou "complexe". Et la différence n'est pas simple. Surtout que le terme CISC regroupe, comme on le verra, des architectures bien différentes.
Les différences entre CISC et RISC
[modifier | modifier le wikicode]La différence la plus intuitive est le nombre d'instructions supporté. Les processeurs CISC supportent beaucoup d'instructions, , alors que les processeurs RISC se contentent d'un petit nombre d'instructions assez simples. Un autre critère, très lié au précédent, est la complexité des instructions en question. Les processeurs CISC incorporent souvent des instructions complexes, comme le calcul de la division, de la racine carrée, de l'exponentielle, des puissances, des fonctions trigonométriques. Plus fréquent, on trouvait des instructions de contrôle élaborées pour gérer les appels de fonction, simplifier l'implémentation des boucles, etc.
La différence principale fait la distinction entre les architectures LOAD-STORE (RISC) et celles qui ne le sont pas (CISC). Les processeurs RISC sont des architectures LOAD-STORE. Sur celles-ci, seules les instructions d'accès mémoire peuvent lire ou écrire en mémoire. Les autres instructions ne peuvent accéder qu'aux registres : elles lisent leurs opérandes dans les registres et enregistrent leur résultat dans les registres. En conséquence, les instructions de calcul ne peuvent prendre que des noms de registres ou des constantes comme opérandes, via les modes d'adressage immédiat et à registre. Pour le dire autrement, elles ne gèrent que les instructions reg-reg et cst-reg, pas les instructions load-op. La distinction se fait uniquement au niveau des instructions de calcul/branchement.
Les premiers processeurs RISC se contentaient de deux instructions d'accès mémoire : LOADq et STORE. Elles ont d'ailleurs donné leur nom à ce type d'architecture. Mais dans les faits, les processeurs RISC modernes peuvent gérer d'autres instructions d'accès à la mémoire. Par exemple, les processeurs ARM7 supportent des instructions de copie mémoire-mémoire nommées CPYP, CPYM et CPYE. Et ce n'est pas incompatible avec un processeur RISC, ni avec une architecture LOAD-STORE. De telles instructions ont beau être assez complexes, ça reste des instructions d'accès mémoire. Même si elles font plusieurs accès mémoire par instruction, c'est comme si on avait fusionné un LOAD suivi d'un STORE en une seule instruction.
Les architectures LOAD-STORE sont les seules à avoir une stricte séparation entre instructions d'accès mémoire et instructions de calcul. À l'opposé, les processeurs CISC ne sont pas des architectures LOAD-STORE, leurs instructions de calcul/branchement peuvent effectuer des accès mémoire, soit pour aller chercher leurs opérandes en RAM, soit pour écrire un résultat. Elles peuvent même en faire plusieurs si plusieurs opérandes sont en mémoire, encore que ce ne soit pas possible sur tous les jeux d'instructions. Au minimum, les processeurs CISC gèrent les instructions load-op. Quelques processeurs CISC anciens supportent tous les modes d'adressage possibles pour les opérandes et le résultat, qui peuvent tous trois être lus/écrits dans les registres ou la RAM.
Les deux propriétés précédentes, à savoir un grand nombre d'instruction et des modes d'adressages complexes, se marient bien avec des instructions de longueur variable. Les instructions d'un processeur CISC sont de taille variable pour diverses raisons, mais la variété des modes d'adressage y est pour beaucoup. Quand une même instruction peut incorporer soit deux noms de registres, soit deux adresses, soit un nom de registre et une adresse, sa taille ne sera pas la même dans les trois cas. Les processeurs RISC ne sont pas concernés par ce problème. Les instructions de calcul n'utilisent que deux-trois modes d'adressages simples, qui demandent d'encoder des registres ou des constantes immédiates de petite taille. Les autres instructions se débrouillent avec des adresses et éventuellement un nom de registre. Le tout peut être encodé sur 32 ou 64 bits sans problèmes.
| Propriété | CISC | RISC |
|---|---|---|
| Instructions |
|
|
| Modes d'adressage |
|
|
| Registres |
|
|
Les performances relatives des CISC/RISC
[modifier | modifier le wikicode]Pour comprendre quels sont les avantages et désavantages des processeurs CISC comparé au RISC, nous allons étudier leur performance et leur code density.
Pour ce qui est de la performance, les choses ne sont pas clairement tranchées. Pour comprendre pourquoi, nous allons partir d'une équation déjà abordée dans un chapitre antérieur, qui donne le temps que met un programme à s'exécuter. Le temps que met un programme pour s’exécuter est le produit :
- du nombre moyen d'instructions exécutées par le programme ;
- de la durée moyenne d'une instruction, en seconde.
- , avec N le nombre moyen d'instruction du programme et la durée moyenne d'une instruction.
Le nombre moyen d'instructions exécuté par un programme s'appelle l'Instruction path length, ou encore longueur du chemin d'instruction en français. Si on utilise le nombre moyen d’instructions, c'est car il n'est pas forcément le même d'une exécution à l'autre, notamment en présence de conditions ou de boucles.
Le processeurs CISC intègrent des instructions complexes, que les processeurs RISC doivent émuler à partir d'une suite d'opérations plus simples. Par exemple, les instructions load-op sont émulées avec une instruction LOAD suivie d'une instruction de calcul. En clair, les processeurs CISC ont un avantage pour le paramètre N, la longueur du chemin d'instruction. Par contre, il n'est pas garantit que les instructions complexes soient aussi rapides que la suite d'instruction RISC équivalente, car les processeurs RISC ont un avantage pour le terme . Voyons pourquoi.
Les processeurs CISC gèrent un grand nombre d'instructions et de modes d'adressage, ce qui les rend plus gourmands en transistors. Non pas qu'il faille ajouter plus de circuits de calcul, les CISC se débrouillent bien avec les circuits usuels pour les 4 opérations basiques. Le vrai problème est le support des nombreux modes d'adressage, des instructions de taille variable, et le grand nombre de variantes de la même opération. En conséquence, les circuits de contrôle du processeur sont plus complexes et utilisent beaucoup de transistors. Le budget en transistors utilisé pour les circuits de contrôle ne sont pas disponibles pour autre chose, comme de la mémoire cache ou des registres. De plus, cela a tendance à limiter la fréquence du processeur.
Les processeurs RISC sont eux plus économes, ils ont moins d'instructions, moins de circuits de contrôle. Ils peuvent utiliser plus de transistors pour autre chose, souvent de la mémoire cache ou des registres. Les processeurs RISC peuvent se permettre d'utiliser beaucoup de registres, ils ont le budget en transistor pour. Et c'est sans compter que la simplicité des circuits de contrôle et des connexions intra-processeur (le bus interne au CPU qu'on verra dans quelques chapitres) fait qu'on peut faire fonctionner le processeur à plus haute fréquence.
Si les registres sont plus nombreux sur les architectures RISC, ce n'est pas qu'une question de budget en transistors. Une autre raison est que les architectures LOAD-STORE ont besoin de plus de registres pour faire le même travail que les architectures à registres, du fait de l'absence d'instructions load-op. La register pressure est donc légèrement plus importante, ce qui est compensée en ajoutant des registres.
La densité de code relative entre RISC et CISC
[modifier | modifier le wikicode]Si la performance ne donne pas de vainqueur clair entre CISC et RISC, il y a cependant un second critère sur lequel la victoire est bien plus nette. Il s'agit de la densité de code, à savoir la taille des programmes mesurée en octets. De nos jours, la taille des programmes n'est pas importante au point d'être un problème, ce n'est pas ca qui prendra plusieurs gibioctets de mémoire. Par contre, au tout début de l'informatique, la mémoire était chère et limitée, avoir des programmes petits était un avantage clair.
La taille d'un programme dépend de deux choses : le nombre d'instructions et leur taille. Et ces deux paramètres sont influencés par le caractère CISC/RISC. Comme dit plus haut, les architectures CISC ont un avantage pour le nombre d'instruction N, du fait de la présence d'instructions load-op et d'instructions complexes. Mais elles ont aussi un avantage pour la taille des instructions. Les instructions des processeurs CISC sont de taille variable, là où celles des processeurs RISC sont de taille fixe. Les instructions CISC ont une taille allant de très courtes à très longues, celles des RISC sont de taille moyenne. Mais le fait d'avoir des instructions très courtes l'emporte sur les processeurs CISC.
Une minorité de processeurs RISC arrivent à obtenir une densité de code proche de celle des processeurs CISC. Pour cela, ils ont classes d'instructions de taille différente. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. De telles instructions de taille quasi-variables permettent de grandement gagner en densité de code.
Il faut noter que sur les processeurs modernes, avoir une bonne densité de code a un impact secondaire sur les performances. En effet, les processeurs modernes ont une mémoire cache qui sert à la fois pour les données, mais aussi pour les instructions chargées depuis la mémoire. Les instructions sont conservées dans le cache après leur exécution, pour une exécution ultérieure (ce qui implique une boucle). Si une instruction est dans le cache, elle est lue directement depuis le cache, sans passer par un long accès en mémoire RAM. Les processeurs modernes ont notamment un petit cache d'instruction, spécialement dédié aux instructions. Plus la densité de code est bonne, plus le cache d'instruction pourra contenir d'instructions, plus il sera efficace, plus il filtrera d'accès mémoire.
Un petit historique de la distinction entre processeurs CISC et RISC
[modifier | modifier le wikicode]Les jeux d'instructions CISC sont les plus anciens et étaient à la mode jusqu'à la fin des années 1980. À cette époque, on programmait rarement avec des langages de haut niveau et beaucoup de programmeurs codaient en assembleur. Avoir un jeu d'instruction complexe, avec des instructions de "haut niveau" facilitait la vie des programmeurs. Et leur meilleure densité de code était un sérieux avantage, car la mémoire était rare et chère.
Au cours des années 70-80, la mémoire est devenue moins chère et les langages de haut niveau sont devenus la norme. Le contexte technologique ayant changé, les processeurs CISC étaient à réévaluer. Est-ce que les instructions complexes des processeurs CISC sont vraiment utiles ? Pour le programmeur qui écrit ses programmes en assembleur, elles le sont. Mais avec les langages de haut niveau, la réponse dépend de l'efficacité des compilateurs. Des analyses assez anciennes, effectuées par IBM, DEC et quelques laboratoires de recherche, ont montré que les compilateurs utilisaient les instructions load-op correctement, mais n'utilisaient pas les instructions complexes. Les analyses plus récentes fournissent la même conclusion. La faible densité de code n'était pas un problème, vu que les mémoires ont une capacité suffisante, encore que ce soit à nuancer.
L'idée de créer des processeurs RISC commença à germer. Mais les processeurs RISC durent attendre un peu avant de percer. Par exemple, l'IBM 801, un processeur au jeu d'instruction très sobre, fût un véritable échec commercial. C'est dans les années 1980 que les processeurs possédant un jeu d'instruction simple devinrent à la mode. Cette année-là, un scientifique de l'université de Berkeley décida de créer un processeur possédant un jeu d'instruction contenant seulement un nombre réduit d'instructions simples, possédant une architecture particulière. Ce processeur était assez novateur et incorporait de nombreuses améliorations qu'on retrouve encore dans nos processeurs haute performances actuels, ce qui fit son succès : les processeurs RISC étaient nés.
Les CISC et les RISC ont chacun des avantages et des inconvénients, qui rendent le RISC/CISC adapté ou pas selon la situation. Par exemple, on mettra souvent un processeur RISC dans un système embarqué, devant consommer très peu et être peu cher. Mais de nos jours, la performance d'un processeur dépend assez peu du fait que le processeur soit un RISC ou un CISC. Les processeurs modernes disposent de tellement de transistors qu'implémenter des instructions complexes a un impact négligeable. Pour donner une référence, le support des instructions complexes sur les processeurs x86 est estimé à 2 à 3% des transistors de la puce, alors que 50% du budget en transistor des processeurs modernes part dans le cache.
Les processeurs actuels sont de plus en plus difficiles à ranger dans des catégories précises. Les processeurs actuels sont conçus d'une façon plus pragmatique : au lieu de respecter à la lettre les principes du RISC et du CISC, on préfère intégrer les instructions qui fonctionnent, peu importe qu'elles viennent de processeurs purement RISC ou CISC. Par exemple, les processeurs ARM récents ont intégré des instructions de copie mémoire, qui sont assez borderline et sont plutôt attendues sur les processeurs CISC.
En parallèle des architectures CISC et RISC, d'autres classes de jeux d'instructions sont apparus. Nous verrons beaucoup de jeux d'instructions dans la suite du cours. Entre les architectures à capacité, les processeurs VLIW, les architectures dataflow, les processeurs SIMD, les Digital Signal Processor, les transport-triggered architectures, les architectures associatives, les architectures neuromorphiques et les achitectures systoliques, il y aura de quoi faire. La plupart de ces jeux d'instructions sont implantés dans des processeurs spécialisés, qu'on fabrique pour une utilisation particulière. Malheureusement, nous ne pouvons pas en parler pour le moment. La suite de ce chapitre va se focaliser sur des jeu d'instruction proches du RISC ou du CISC.
Les processeurs avec plusieurs jeu d'instruction
[modifier | modifier le wikicode]Il faut signaler que certains processeur supportent plusieurs jeu d'instruction à la fois. Les processeurs x86 des PC sont dans ce cas, mais aussi les processeurs ARM utilisés partout ailleurs.
La compatibilité entre 32 et 64 bits
[modifier | modifier le wikicode]Le cas des processeurs x86 est assez simple à comprendre. Ils ont beaucoup évolués dans le temps, commençant comme des processeurs 8 bits, puis 16 bits, suivis par des 32 bits et enfin des processeurs 64 bits. Et à chaque transition, le jeu d'instruction a évolué. Le x86 8 bit n'est pas le même que le x86 16 bits, qui est lui-même différent du x86 32 bits, etc.
La raison est que pour étendre les adresses, il fallait modifier l'encodage des instructions. Et quitte à modifier l'encodage des instructions, autant en rajouter d'autres, et supprimer des instructions devenus inutiles. Une autre raison est qu'Intel et AMD ont rajouté des registres lors du passage au 32 bits, et idem lors du passage au 64 bits. Le jeu d'instruction x86 16 bits avait trop peu de registres pour des besoins actuels, ce qui fait qu'Intel et AMD ont décidé d'en rajouter. Et vu que rajouter des registres impactait l'encodage des instructions, cela ne pouvait se faire qu'à l'occasion d'un changement de jeu d'instruction.
Une dernière raison est que la mémoire virtuelle n'est pas gérée de la même manière entre les processeurs 16, 32 et 64 bits. Mais nous ne pouvons en parler à ce stade du cours. Mais là encore, cela impacte le jeu d'instruction et le modèle mémoire du processeur, bien que d'une manière assez limitée. Cela revient à ajouter des registres, en supprimer d'autres, modifier l'interprétation de registres existants, etc.
Ces changements se sont fait progressivement, ce qui fait que les jeux d'instruction 8/16/32/64 bits se ressemblent beaucoup, mais au point d'être parfaitement compatible. Exécutez un code prévu pour un CPU 16 bits sur un CPU 32 bits, vous aurez des problèmes. Pour éviter cela, les processeurs x86 supportent les différents modes d’instruction existants, par souci de compatibilité. Il est possible de configurer un processeur x86 en mode 16 bits, pour lui faire exécuter du code 16 bits directement, sans interprétation ou émulation d'instruction via exceptions matérielles. Ou encore, on peut mettre un CPU 64 bits en mode 32 bits, pour qu'il exécute du code 32 bits. Les modes en question sont :
- le mode réel en 8 et 16 bits ;
- le mode protégé 16 bit et sa variante 32 bits ;
- le mode long en 64 bits, avec deux sous-modes de compatibilité pour exécuter du code 16 et 32 bits.
Si l'ordinateur dispose d'un BIOS, il est démarré en mode réel, puis il passe en mode long pour laisser la main à un système d'exploitation 64 bits. Le mode protégé n'est pas utilisé, sauf éventuellement pour des besoins d'émulation. Si l'ordinateur dispose d'un UEFI, le processeur est lancé en mode long et y reste, que ce soit pour l'UEFI ou le système d'exploitation ou autre. Les modes réels et protégés ne sont utilisés que quand il faut émuler du vieux code.
Il existe d'autres modes, comme le System Management Mode qui laisse la main au BIOS/UEFI, et un mode Virtual 8086 qu'on ne peut pas expliquer à ce moment. Les deux seront cependant détaillés dans quelques chapitres. Le System Management Mode sera abordé dans le chapitre sur les interruptions, le mode Virtual 8086 sera abordé dans le chapitre sur la virtualisation accélérée en matériel.

Les processeurs ARM sont aussi dans ce cas. Leur jeu d'instruction de base a beaucoup évolué dans le temps. L'ARM de base était un jeu d'instruction 32 bits, mais une variante 64 bits a vu le jour par la suite. Comme pour les CPU x86, les CPU ARM 64 bits supportent deux modes d'exécution : un 32 bits et un 64 bits, là encore pour des questions de compatibilité.
Les jeux d'instruction compacts des processeurs RISC
[modifier | modifier le wikicode]Quelques CPU RISC 32 bits supportaient un jeu d'instruction compact, en plus de leur d'instruction classique. Il s'agissait d'un second jeu d’instruction avec une densité de code excellente, qui avait pour but de diminuer la taille des programmes. La technique a été introduite sur des CPU 32 bits, et le jeu d'instruction utilisait des instructions codées sur 16 bits. L'exemple type est celui des Les processeurs ARM, qui incorporent un jeu d'instruction compact appelé thumb, que nous allons détailler dans ce qui suit.
Le mode thumb était présent sur les processeurs ARM 32 bits, mais il a été conservé lors du passage au 64 bits. Le thumb encode les instructions sur 16 bits, soit deux fois moins que l'ARM 32 bits normal. Il ne s'agissait pas d'un jeu d'instruction totalement séparé de l'ARM normal, mais d'un sous-ensemble de l'ARM normal, utilisant un encodage différent. Concrètement, toute instruction thumb 16 bits a un équivalent en ARM 32 bits. Par contre, la réciproque n'est pas vraie. Et l'encodage des deux instructions n'est pas le même, ce qui est évident vu que les instructions thumb sont encodées sur 16 bits et les équivalentes sur 32.
Les instructions thumb ont des limitations que les instructions ARM 32 bits n'ont pas. Par exemple, les instructions thumb ont un accès restreint aux registres de status et du co-processeur. De plus, la majorité des instructions thumb n'ont accès qu'à la moitié des registres du processeur. Les instructions arithmétiques et les décalages sont séparés en mode thumb, alors que chaque instructions arithmétiques peut ajouter un décalage à une opérande en mode ARM 32 bits. Les instructions pour le co-processeur ne sont pas disponibles en thumb.
Autre exemple : la gestion des instructions à prédicats. L'ARM 32 bits normal utilise la prédication totale, à savoir que toutes les instructions s'exécutent ou non selon la valeur d'un résultat de comparaison, stocké dans un registre de prédicat. Toute instruction encode donc quelques bits pour choisir ce registre. Cela est supprimé avec thumb. A part quelques branchements, les instructions ne sont pas prédicatées et s'exécutent inconditionnellement.
Les deux jeux d'instruction ont un mode d'exécution chacun, ce qui veut dire que le processeur est configuré pour fonctionner en mode thumb ou en mode ARM 32 bits. En clair, on ne peut pas mélanger les deux jeu d'instructions en même temps. La raison est qu'une même suite de bit correspondait soit à une instruction ARM normale, soit à deux instructions thumbs. Pour faire la différence, il faut préciser dans quel mode d'exécution le CPU est : mode ARM ou mode thumb. Le processeur interprète les instructions correctement en tenant compte de ce mode. Niveau implémentation, la gestion des instructions thumb est assez simple : le processeur convertit les instructions thumb en leur équivalent ARM 32 bits, juste après les avoir chargées.
Une seconde version du jeu d'instruction thumb a vu le jour, sur les processeurs ARMv6T2, avec thumb-2. Elle avait pour particularité d'utiliser des instructions de taille variable. Rien d'aussi développé que ce qu'on a sur les CPU CISC, comme les CPU x86. Les instructions ARM thumb-2 faisaient soit 16 bits, soit 32 bits. Concrètement, les instructions 16 bits de thumb-1 étaient reprises à l'identique, thumb-2 se contentait d'ajouter des instructions en plus codées sur 32 bits. Les instructions ajoutées étaient des instruction mémoire, des instructions de configuration du CPU, des instructions de manipulation de bit, et quelques autres.
Un ajout particulièrement notable est l'ajout d'une instruction IT, qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse.
Il faut noter que quelques processeurs ARM implémentent uniquement le jeu d'instruction thumb-1/2, pas l'ARM 32 bits normal. Ce sont des processeurs faible performance, à destination de systèmes embarqués, qui ont besoin d'une densité de code correcte, comme les CPU ARM Cortex série M.
- Il est arrivé que certains CPU ARM en supportent trois : le jeu d'instruction ARM 32 bits classique, le mode Thumb qu'on décrira plus bas, et une extension nommée Jazelle JBX qui accélérait les programmes écrits en Java (elle exécutait du bytecode JAVA sur le processeur, nous détaillerons cela dans un autre chapitre).
Les CPU ARM n'étaient pas les seuls à utiliser la même technique. Les CPU SuperH d'Hitachi ont été les premiers à implémenter cette technique, avant ARM. Lors du passage de 16 à 32 bits, ces CPU Hitachi ont ajouté un jeu d'instruction de 16 bits à leurs CPU 32 bits, très similaire au jeu d'instruction 16 bits d'avant. Et c'est ce jeu d'instruction compact qui a servi d'inspiration à ARM. Les CPU MIPS ont tenté la même approche, avec l'inclusion du jeu d’instruction MIPS16e, de 16 bits lui aussi.