# Fonctionnement d'un ordinateur/Les jeux d'instructions

Les instructions d'un processeur dépendent fortement du processeur utilisé. La liste de toutes les instructions qu'un processeur peut exécuter s'appelle son jeu d'instructions. Celui-ci définit les 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

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

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. A 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.
Différences CISC/RISC :
| Propriété | CISC | RISC |
| Instructions | Nombre d'instructions élevé, parfois plus d'une centaine. Parfois, présence d'instructions complexes (fonctions trigonométriques, gestion de texte, autres). Supportent des types de données complexes : texte, listes chainées, etc. Instructions de taille variable, pour améliorer la densité de code. | Faible nombre d'instructions, moins d'une centaine. Pas d'instruction complexes. Types supportés limités aux entiers (adresses comprises) et flottants. Instructions de taille fixe pour simplifier le processeur. |
| Modes d'adressage | Support des instructions load op, systèmatique Souvent, support d'instructions multi-accès (qui font plusieurs accès mémoires par instruction). | Architecture LOAD-STORE. Un seul accès mémoire par instruction maximum |
| Registres | Présence de registres spécialisés, parfois absence de registres généraux. Peu de registres : rarement plus de 16 registres entiers. | Presque pas de registres spécialisés. Beaucoup de registres, souvent plus de 32. |

### Les contraintes d'implémentation sont différentes

Les processeurs CISC sont naturellement plus complexes que les processeurs RISC, et cela a un impact sur leur conception. Le grand nombre d'instructions fait qu'on doit câbler beaucoup de circuit pour gérer toutes ces instructions. Non pas qu'il faille forcément beaucoup de circuits de calcul, les CISC ne gère pas tant que d'opérations complexes. Par contre, le support des nombreux modes d'adressage, des instructions de taille variable, et le grand nombre de variantes de la même opération font que les circuits de contrôle du processeur sont plus complexes, utilisent beaucoup de transistors et d'énergie. Et les transistors utilisés pour ces instructions ne sont pas disponibles pour autre chose, comme de la mémoire cache. La difficulté de conception de ces processeurs était aussi sans précédent.
Les processeurs RISC sont eux plus économes, ils ont moins d'instructions, donc moins de circuits de contrôle, et peuvent utiliser plus de transistors pour autre chose. Le autre chose est généralement du cache, mais aussi des registres. Les processeurs RISC peuvent se permettre d'utiliser beaucoup de registres, ils ont le budget en transistor pour, 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) rendent la chose plus aisée.
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. En effet, les processeurs CISC supportent des modes d'adressage qui permettent de lire une opérande directement depuis la mémoire, sans passer par un registre. Sur une architecture RISC LOAD-STORE, une instruction de ce genre est émulée avec une instruction LOAD et une instruction de calcul, ce qui fait que la donnée lue depuis la mémoire passe explicitement par un registre. La register pressure est donc légèrement plus importante, ce qui est compensée en ajoutant des registres.

### Un petit historique de la distinction entre processeurs CISC et RISC

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 outre ces avantages "humains", les architectures CISC avaient une meilleure densité de code, car un programme codé sur CISC utilise moins d'instructions, sans compter que celles-ci sont de longueur variable. À l'époque, la mémoire était rare et chère, l'économiser était crucial, la densité de code des CISC était parfaitement adaptée.
Mais par la suite, la mémoire est devenue moins chère et sa capacité a augmenté. De plus, les langages de haut niveau sont devenus plus fréquents au cours des années 70-80. Le contexte technologique ayant changé, les avantages des 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 depuis l'invention des 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 n'utilisaient pas la totalité des instructions fournies par un processeur. Les analyses plus récentes fournissent la même conclusion : les compilateurs ou programmeurs n'utilisent pas souvent les instructions complexes, même quand elles peuvent être utiles. Autant dire que beaucoup de transistors étaient gâchés !
L'idée de créer des processeurs RISC commença à germer. Ils n'ont pas les défauts des CISC, mais n'en ont pas les avantages : la densité de code est mauvaise, en contrepartie d'un processeur plus simple à concevoir. Ils étaient plus adaptés au contexte technologique moderne. Avec des programmes provenant de compilateurs, avoir un petit nombre d'instructions aux modes d'adressages simples est plus que largement suffisant. Et ne pas implémenter beaucoup d'instructions permet de dégager un budget en transistor conséquent, qui sert à ajouter des registres ou du cache, améliorant ainsi les performances. La faible densité de code n'est pas un problème, vu que les mémoires ont une capacité suffisante, encore que ce soit à nuancer.
Mais de tels processeurs RISC, complètement opposés aux processeurs CISC, durent attendre un peu avant de percer. Par exemple, IBM décida de créer un processeur possédant un jeu d'instruction plus sobre, l'IBM 801, qui fût un véritable échec commercial. Mais la relève ne se fit pas attendre. 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.
Durant longtemps, les CISC et les RISC eurent chacun leurs admirateurs et leurs détracteurs. Mais de nos jours, on ne peut pas dire qu'un processeur CISC sera toujours meilleur qu'un RISC ou l'inverse. Chacun a 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.
En tout cas, la performance d'un processeur dépend assez peu du fait que le processeur soit un RISC ou un CISC, même si cela peut faire la différence en termes de simplicité de conception. Les processeurs modernes disposent de tellement de transistors qu'implémenter des instructions complexes est négligeable. Pour donner une référence, près de 50% du budget en transistor des processeurs modernes part dans le cache. Et il est estimé que le support des instructions complexes sur les processeurs x86 ne prend que 2 à 3% des transistors de la puce, ce qui est négligeable.
De plus, de nos jours, les différences entre CISC et RISC commencent à s'estomper. 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. Les anciens processeurs RISC se sont ainsi garnis d'instructions et de modes d'adressage de plus en plus complexes et les processeurs CISC ont intégré des techniques provenant des processeurs RISC (pipeline, etc). La guerre RISC ou CISC n'a plus vraiment de sens de nos jours.
En parallèle de ces architectures CISC et RISC, qui sont en quelque sorte la base de tous les jeux d'instructions, d'autres classes de jeux d'instructions sont apparus, assez différents des jeux d’instructions RISC et CISC. On peut par exemple citer le Very Long Instruction Word, qui sera abordé dans les chapitres à la fin du tutoriel. La plupart de ces jeux d'instructions sont implantés dans des processeurs spécialisés, qu'on fabrique pour une utilisation particulière. Ce peut être pour un langage de programmation particulier, pour des applications destinées à un marché de niche comme les supercalculateurs, etc.

## Les jeux d'instructions spécialisés

En parallèle des architectures CISC et RISC, d'autres classes de jeux d'instructions sont apparus. Ceux-ci visent des buts distincts, qui changent suivant le jeu d'instruction :
* soit ils cherchent à diminuer la taille des programmes et à économiser de la mémoire ;
* soit ils cherchent à gagner en performance, en exécutant plusieurs instructions à la fois ;
* soit ils tentent d'améliorer la sécurité des programmes et les rendent résistants aux attaques ;
* soit ils sont adaptés à certaines catégories de programmes ou de langages de programmation.

### Les architectures compactes

Certains chercheurs ont inventé des jeux d’instruction pour diminuer la taille des programmes. Certains processeurs disposent de deux jeux d'instructions : un compact, et un avec une faible densité de code. Il est possible de passer d'un jeu d'instructions à l'autre en plein milieu de l’exécution du programme, via une instruction spécialisée.
D'autres processeurs sont capables d’exécuter des binaires compressés (la décompression a lieu lors du chargement des instructions dans le cache).

### Les architectures parallèles

Certaines architectures sont conçues pour pouvoir exécuter plusieurs instructions en même temps, lors du même cycle d’horloge : elles visent le traitement de plusieurs instructions en parallèle, d'où leur nom d’architectures parallèles. Ces architectures visent la performance, et sont relativement généralistes, à quelques exceptions près. On peut, par exemple, citer les architectures very long instruction word, les architectures dataflow, les processeurs EDGE, et bien d'autres. D'autres instructions visent à exécuter une même instruction sur plusieurs données différentes : ce sont les instructions SIMD, vectorielles, et autres architectures utilisées sur les cartes graphiques actuelles. Nous verrons ces architectures plus tard dans ce tutoriel, dans les derniers chapitres.

### Les Digital Signal Processors

Certains jeux d'instructions sont dédiés à des types de programmes bien spécifiques, et sont peu adaptés pour des programmes généralistes. Parmi ces jeux d'instructions spécialisés, on peut citer les fameux jeux d'instructions Digital Signal Processor, aussi appelés des DSP. Nous reviendrons plus tard sur ces processeurs dans le cours, un chapitre complet leur étant dédié, ce qui fait que la description qui va suivre sera quelque peu succincte. Ces DSP sont des processeurs chargés de faire des calculs sur de la vidéo, du son, ou tout autre signal. Dès que vous avez besoin de traiter du son ou de la vidéo, vous avez un DSP quelque part, que ce soit une carte son ou une platine DVD.
Ls DSP ont un jeu d'instruction similaire aux jeux d'instructions RISC, avec quelques instructions supplémentaires spécialisées pour faire du traitement de signal. On peut par exemple citer l'instruction phare de ces DSP, l'instruction MAD, qui multiplie deux nombres et additionne un 3éme au résultat de la multiplication. De nombreux algorithmes de traitement du signal (filtres FIR, transformées de Fourier) utilisent massivement cette opération. Ces DSP possèdent aussi des instructions dédiées aux boucles, ou des instructions capables de traiter plusieurs données en parallèle (en même temps). De plus, les DSP utilisent souvent des nombres flottants assez particuliers qui n'ont rien à voir avec les nombres flottants que l'on a vu dans le premier chapitre. Certains DSP supportent des instructions capables d'effectuer plusieurs accès mémoire en un seul cycle d'horloge : ils sont reliés à plusieurs bus mémoire et sont donc capables de lire et/ou d'écrire plusieurs données simultanément. L'architecture mémoire de ces DSP est une architecture Harvard, couplée à une mémoire multi-ports. Les caches sont rares dans ces architectures, quoique parfois présents.

### Les architectures dédiées à un langage de programmation

Certains processeurs sont carrément conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des processeurs dédiés. Par exemple, l'ALGOL-60, le COBOL et le FORTRAN ont eu leurs architectures dédiées. Les fameux Burrough E-mode B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau à avoir été directement câblé en assembleur sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du FORTH.
Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leur circuits : elles possédaient notamment un garbage collector câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution.
Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Les programmes compilés faisaient un mauvais usage des instructions machines, ne savaient pas bien utiliser les modes d'adressages adéquats, manipulaient assez mal les registres, etc. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi étés inventées, avant que les concepteurs se rendent compte des défauts de cette approche. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues.
Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle.

### Les implémentations matérielles de machines virtuelles

Comme vous le savez sûrement, les langages de programmation de haut niveau sont traduits en langage machine. La traduction d'un programme en un fichier exécutable est donc un processus en deux étapes. Premièrement, la compilation traduit le code source en langage assembleur, une représentation textuelle du langage machine. Puis l'assembleur est assemblé en langage machine. Les deux étapes sont réalisées respectivement par un compilateur et un assembleur.
Le compilateur ne fait pas forcément la traduction en assembleur directement, la plupart des compilateurs modernes passent par un langage intermédiaire, avant d'être transformés en langage machine. Faire ainsi a de nombreux avantages pour les concepteurs de compilateurs. Notamment, cela permet d'avoir un compilateur qui traduit le langage de haut niveau pour plusieurs jeux d’instructions différents, par exemple un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. Pour cela, le compilateur est composé de deux parties : une partie commune qui traduit le C en langage intermédiaire, et plusieurs back-end qui traduisent le langage intermédiaire en code machine cible.
Le langage intermédiaire peut être vu comme l'assembleur d'une machine abstraite, que l'on appelle une machine virtuelle, qui n'existe pas forcément dans la réalité. Le langage intermédiaire est conçu de manière à ce que la traduction en code machine soit la plus simple possible. Et surtout, il est conçu pour pouvoir être transformé en plusieurs langages machines différents sans trop de problèmes. Par exemple, il dispose d'un nombre illimité de registres. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise des registres ou des adresses, en insérant des instructions d'accès mémoire. Et cela permet de gérer des architectures très différentes qui n'ont pas les mêmes nombres de registres.
Mais si la majorité des langages de haut niveau sont compilés, il en existe qui sont interprétés, c'est à dire que le code source n'est pas traduit directement, mais transformé en code machine à la volée. Là encore, on trouve un langage intermédiaire appelé le bytecode . Le langage de haut niveau est traduit en bytecode, via un processus de pré-compilation, et c'est ce bytecode qui est "exécuté". Plus précisément, le bytecode est passé à un logiciel appelé l'interpréteur, qui lit une instruction à la fois dans le bytecode et exécute une instruction équivalente sur le processeur. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le bytecode est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur.
L'avantage est celui de la portabilité, à savoir qu'un même code peut tourner sur plusieurs machines différentes. Le code machine est spécifique à un jeu d’instruction, la compatibilité est donc limitée. C'est très utilisé par certains langages de programmation comme le Python ou le Java, afin d'obtenir une bonne compatibilité : on compile le Python/Java en bytecode, qui lui-même est interprété à l’exécution. Tout ordinateur sur lequel on a installé une machine virtuelle Java/Python peut alors exécuter ce bytecode.
Le bytecode est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Le processeur en question n'existe pas forcément, mais il est possible de décrire son jeu d'instruction en détail. Lesbytecode assez anciens sont conçus pour un type d'architecture spécifique, appelé une machine à pile, qu'on verra dans quelques chapitres. Elles ont la particularité de ne pas avoir de registres et ont un avantage quant à la taille du bytecode. Il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés.
Si le jeu d'instruction d'un bytecode est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la machine SECD, qui sert de langage intermédiaires pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le bytecode du langage FORTH.
Mais le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le bytecode Java est compilé ou interprété, ce qui permet au bytecode Java d'être portable sur des architectures différentes. Mais certains processeurs ARM, qu'on trouve dans des système embarqués, sont une implémentation matérielle de la machine virtuelle Java. Leur langage machine est le bytecode Java lui-même, ce qui supprime l'étape d'interprétation. L'intérêt de ce genre de stratagèmes reste cependant mince. Cela permet d’exécuter plus vite les programmes compilés en bytecode, comme des programmes Java pour la JVM Java, mais cela n'a guère plus d'intérêt.