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 anciens macintoshs (la génération de macintosh produits entre 1994 et 2006) utilisaient un jeu d'instruction différent : le PowerPC (depuis 2006, les macintoshs utilisent un processeur X86). 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.
L'adressage des opérandes.
La première classification que nous allons voir est basée sur l'adressage des opérandes. Le critère de distinction est les modes d'adressage autorisés pour les opérandes. Les accès mémoire et branchements ne sont pas impliqués dans cette classification. La raison à cela est que les branchements ont des modes d'adressages dédiés, tandis que les accès mémoire ont besoin de beaucoup de modes d'adressage pour faire leur travail. Tel n'est pas le cas des instructions de calculs, qui peuvent utiliser un nombre limité de modes d'adressage sans le moindre problèmes. Certaines architectures limitent les modes d'adressages pour les opérandes, afin de simplifier le processeur ou le travail du compilateur. D'autres ont utilisent beaucoup de modes d'adressage, dans un souci de flexibilité ou de performances. Bref, voyons comment les différents types d'architectures gèrent les modes d'adressages pour les opérandes.
Dans les grandes lignes, on trouve trois catégories principales : les architectures mémoire-mémoire, les architectures à registres et les architectures FIFO/LIFO. Dans les architectures mémoire-mémoire, les opérandes sont lues directement depuis la mémoire RAM, sans intermédiaires. Pour les architectures à registres, le processeur stocke ses opérandes dans des registres. Enfin, les architectures à pile et à file utilisent une mémoire FIFO ou LIFO pour stocker les opérandes.
Les architectures à registres sont subdivisées en sous-catégories : les architectures LOAD-STORE, les architectures à accumulateurs et les architectures à registre générales. Pour résumer, nous allons parler des architectures suivantes : les machines à pile, les machines à file, les architectures à accumulateur, les architectures mémoire-mémoire et les architectures à registres.
Les architectures mémoire-mémoire.
Les toutes premières machines n'avaient pas de registres pour les données et ne faisaient que manipuler la mémoire RAM ou ROM : on parle d'architectures mémoire-mémoire. Dans cette architecture ci, il n'y a pas de registres généraux : les instructions n'accèdent qu'à la mémoire principale. Néanmoins, les registres d'instruction et pointeur d'instruction existent toujours. Les seules opérandes possibles pour ces processeurs sont des adresses mémoire, ce qui fait qu'un mode d'adressage est très utilisé : l'adressage absolu.
Ces architectures avaient l'avantage d'avoir une gestion de la mémoire assez simple. Le nombre d'instruction d'accès mémoire était assez important, en raison de l'absence de registres, chose essentielle sur les architectures modernes. Ce genre d'architectures est aujourd'hui tombé en désuétude depuis que la mémoire est devenue très lente comparé au processeur.
Les architectures à registres.
Les architectures mémoire-mémoire ont un défaut rédhibitoire : elles n'ont pas de registres pour stocker leurs opérandes. La conséquence est que les performances sont mauvaises, la RAM étant assez lente par rapport aux registres du processeur. Et encore une fois, les chercheurs et ingénieurs ont inventé des architectures qui résolvent ce problème : les architectures à registres. Celles-ci possèdent des registres qui permettent de conserver temporairement une opérande destinée à être utilisée souvent, ou des résultats de calculs temporaires. Il en existe plusieurs sous-types, qui se distinguent par leur nombre de registres pour les opérandes et par leurs modes d'adressages.
Les architectures à accumulateur.
Les architectures à accumulateur sont des architectures à registres avec un unique registre pour les opérandes, appelé l'accumulateur. L'accumulateur mémorise une opérande et le résultat de l'instruction est automatiquement mémorisé dans l'accumulateur. Si l'instruction manipule plusieurs opérandes, les opérandes qui ne sont pas dans l'accumulateur sont lus depuis la mémoire ou des registres séparés de l'accumulateur. L'accumulateur est adressé grâce au mode d'adressage implicite, de même que le résultat de l'opération. Par contre, les autres opérandes sont localisées avec d'autres modes d'adressage : absolu pour le cas le plus fréquent, inhérent (à registre) sur certaines architectures, indirect à registre pour d'autres, etc.
Historiquement, les premières architectures à accumulateur ne contenaient aucun autre registre que l'accumulateur. Toutes les opérandes hors-accumulateur étaient lues en mémoire. Sur ces processeurs, les modes d'adressages supportés étaient les modes d'adressages implicite, absolus, et immédiat. Ces architectures sont parfois appelées architectures 1-adresse, pour une raison simple : la majorité des instructions manipulent deux opérandes, ce qui fait qu'elles devaient lire une opérande depuis la RAM. Pour ces opérations, le résultat ainsi qu'une des opérandes sont stockés dans l'accumulateur, et adressés de façon implicite, seule la seconde opérande étant adressée directement.
Avec ces seuls modes d'adressages, l'utilisation de tableaux ou de structures était un véritable calvaire. Pour améliorer la situation, les processeurs à accumulateurs ont alors incorporés des registres d'Index, pour faciliter les calculs d'adresse mémoire. Ils étaient capables de stocker des indices de tableaux, ou des constantes permettant de localiser une donnée dans une structure. Au départ, ces processeurs n'utilisaient qu'un seul registre d'Index, accessible et modifiable via des instructions spécialisées, qui se comportait comme un second accumulateur spécialisé dans les calculs d'adresses mémoire. Les modes d'adressages autorisés restaient les mêmes qu'avec une architecture à accumulateur normale. La seule différence, c'est que le processeur contenait de nouvelles instruction capables de lire ou d'écrire une donnée dans/depuis l'accumulateur, qui utilisaient ce registre d'Index de façon implicite. Mais avec le temps, nos processeurs finirent par incorporer plusieurs de ces registres. Nos instructions de lecture ou d'écriture devaient alors préciser quel registre d'Index utiliser. Le mode d'adressage "Indexed Absolute" vit le jour. Les autres modes d'adressages, comme le mode d'adressage Base + Index ou indirects à registres étaient plutôt rares à l'époque et étaient difficiles à mettre en œuvre sur ce genre de machines.
Ensuite, ces architectures s’améliorèrent un petit peu : on leur ajouta des registres capables de stocker des données. L’accumulateur n'était plus seul au monde. Mais attention : ces registres ne peuvent servir que d’opérande dans une instruction, et le résultat d'une instruction ira obligatoirement dans l'accumulateur. Ces architectures supportaient donc le mode d'adressage inhérent.
Les architectures à registres.
Les processeurs à registres peuvent stocker temporairement des données dans des registres généraux ou spécialisés. Pour échanger des données entre la mémoire et les registres, on peut utiliser une instruction à tout faire : MOV. Sur d'autres, on utilise des instructions séparées pour copier une donnée de la mémoire vers un registre (LOAD), copier le contenu d'un registre dans un autre, copier le contenu d'un registre dans la mémoire RAM (STORE), etc.
Ces architectures à registres généraux (ainsi que les architectures "Load-Store" qu'on verra juste après) sont elles-même divisées en deux sous-classes bien distinctes : les architectures 2 adresses et les architectures 3 adresses. Cette distinction entre architecture 2 et 3 adresses permet de distinguer les modes d'adressages des opérations arithmétiques manipulant deux données : additions, multiplications, soustraction, division, etc. Ces instructions ont donc besoin d'adresser deux données différentes, et de stocker le résultat quelque part. Il leur faut donc préciser trois opérandes dans le résultat : la localisation des deux données à manipuler, et l'endroit où ranger le résultat.
Les architectures LOAD-STORE.
Les architectures LOAD-STORE sont identiques aux architectures à registres à un détail près : les instructions arithmétiques et logiques ne peuvent aller chercher leurs données que dans des registres du processeurs. Dit autrement, seules les instructions d'accès mémoire peuvent accéder à la mémoire, les autres instructions n’accédant pas directement à la mémoire. En conséquence, ces instructions ne peuvent prendre que des noms de registres ou des constantes comme opérandes : cela n'autorise que les modes d'adressage immédiat et à registre. Il faut noter aussi que les architectures Load-store sont elles aussi classées en architectures à 2 ou 3 adresses, comme les architectures à registres.
Les architectures à pile et à file.
Les dernières architectures que nous allons voir sont les machines à pile et les machines à file des jeux d'instructions où les registres sont remplacés par une mémoire tampon dédiée aux opérandes de calculs. Il s'agit d'une mémoire de type FIFO pour une machine à file, alors que c'est une LIFO pour une machine à pile. La FIFO/LIFO est placée dans le processeur sur certaines machines, alors que d'autres la placent dans la mémoire RAM. Dans le premier cas, la mémoire tampon sert d'intermédiaire entre la mémoire RAM et le reste du processeur. Dans les deux cas, les opérandes sont stockées dans la mémoire tampon dans leur ordre d'ajout et chaque opérande prend exactement un byte, pas plus ni moins. Les opérandes sont chargées par certaines instructions dans la FIFO/LIFO et y sont placées les unes à la suite des autres. De plus, quand le processeur fait un calcul quelconque, le résultat est ajouté à la pile/file. La pile/file se remplit donc progressivement d'opérandes chargées volontairement ou de résultats de calculs. Les seules opérandes manipulables par une instruction sont placées au sommet de la pile ou file : il faut ajouter les opérandes unes par unes avant d'exécuter l'instruction. L'instruction dépile automatiquement les opérandes qu'elle utilise et empile son résultat. 
Sur certaines machines FIFO/LIFO, la pile et la file sont localisées non pas dans le processeur, mais dans la mémoire RAM. Ces machines ont besoin d'un registre pour stocker l'adresse du sommet de la pile/file. Dans le cas d'une machine à pile, celui-ci est appelée le "stack pointer", ou pointeur de pile. Sur les machines à file, il est appelé le "queue pointer", ou encore le pointeur de file. Les instructions de calcul utilisent uniquement les registres de pile pour savoir où se trouvent les opérandes. En clair, toutes les instructions utilisent uniquement le mode d'adressage implicite. Il va de soit que pour simplifier la conception du processeur, celui-ci peut contenir des registres internes pour stocker temporairement les opérandes des calculs, mais ces registres ne sont pas accessibles au programmeur : ce ne sont pas des registres architecturaux. Par exemple, sur certaines machines à pile, tout ou partie de la pile est stockée directement dans des registres internes au processeur, pour gagner en performance.
Les instructions des machines à pile.
Ces jeux d’instructions ont des instructions pour ajouter des opérandes dans la FIFO/LIFO ou en retirer. Dans ce qui va suivre, nous allons prendre l'exemple d'une machine à pile, mais il existe des équivalents pour les machines à file. Dans les grandes lignes, il y a deux instructions principales nommées PUSH et POP pour l'ajout et le retrait d'opérandes dans la pile, ainsi que quelques instructions annexes sur certaines machines. Les machines à file possèdent des instructions équivalentes, à savoir deux instructions "Queue" et "Unqueue" pour ajouter ou retirer une opérande dans la file, ainsi que quelques instructions annexes. Voici les deux instructions principales de gestion de la pile, présentes sur toutes les architectures à pile :
Vu qu'une instruction dépile ses opérandes, on ne peut pas les réutiliser. Ceci dit, certaines instructions ont été inventées pour limiter la casse : on peut notamment citer l'instruction DUP, qui copie le sommet de la pile en deux exemplaires. On peut aussi citer l'instruction SWAP, qui échange deux données dans la pile.
Enfin, on trouve des instructions pour sauvegarder la totalité de la pile ou de la file. Ces instructions servent surtout pour les appels de fonctions/sous-programmes, chacun d'entre eux demandant de vider complètement les registres du processeur, ainsi que la file ou la pile. Ils sont l'équivalent sur ces architectures des instructions de spilling, qui sauvegardent la totalité des registres du processeur en mémoire RAM, pour les récupérer à la fin de l’exécution de la fonction.
Les modes d'adressage des machines à pile/file.
Les machines à pile que je viens de décrire ne peuvent manipuler que des données sur la pile : ces machines à pile sont ce qu'on appelle des machines à zéro adresse. Avec une telle architecture, les instructions sont très petites, car il n'y a pas besoin de bits pour indiquer la localisation des données dans la mémoire, sauf pour POP et PUSH. Toutefois, certaines machines à pile plus évoluées autorisent certaines instructions à préciser l'adresse mémoire d'un (voire plusieurs, dans certains cas) de leurs opérandes. Ces machines sont appelées des machines à pile à une adresse.
En théorie, les instructions qui manipulent des opérandes ne peuvent qu’accéder aux instructions au sommet de la pile, ou la fin de la file. Ces opérandes sont retirées de la pile par l'instruction et le résultat est placé au sommet de la pile. C'est du moins le cas sur les machines à pile dites "pures", mais d'autres architectures ne fonctionnent pas ainsi. Avec elles, il est possible de préciser la position dans la pile des opérandes à utiliser. Une instruction peut donc indiquer qu'elle veut utiliser les opérandes situées 2, 3 ou 5 cases sous le sommet de la pile. Si les opérandes sélectionnées ne sont pas toujours retirées de la pile (tout dépend de l'architecture), le résultat est lui toujours placé au sommet de la pile. Les opérandes se déplacent donc bizarrement dans ce genre d'architectures. Ces jeux d'instruction n'utilisent pas la pile comme une mémoire LIFO, ce qui lui vaut le nom d'architecture à pseudo-pile. Il existe un équivalent pour les machines à file, qui portent encore une fois le nom de machines à file "impures" ou à pseudo-file.
Avantages et désavantages.
Sur ces architectures, les programmes utilisent peu de mémoire. La raison à cela est que les instructions sont très petites : on n'a pas besoin d'utiliser de bits pour indiquer la localisation des données dans la mémoire, sauf pour Pop et Push. Vu que les programmes créés pour ces machines sont souvent très petits, on dit que la densité du code ("code density") est bonne. Par contre, une bonne partie des instructions de notre programmes seront des instructions Pop et Push qui ne servent qu'à déplacer les opérandes entre la RAM et la FIFO/LIFO. Une bonne partie des instructions ne sert donc qu'à manipuler la mémoire et pas à faire des calculs. Sans compter que notre programme comprendra beaucoup d'instructions comparé aux autres types de processeurs. Enfin, ces machines n'ont pas besoin d'utiliser beaucoup de registres pour stocker leur état : un "Stack Pointer" et un "Program Counter" suffisent. Les machines à pile furent les premières à être inventées et utilisées : dans les débuts de l’informatique, la mémoire était rare et chère, et l'économiser était important. Ces machines à pile permettaient d'économiser de la mémoire facilement et d'utiliser peu de registres, ce qui était le compromis idéal pour l'époque.
Les architectures à pile et à file ont plusieurs défauts. En premier lieu, la complexité de la gestion de la pile/file entraîne l'usage d'un grand nombre d'instructions d'accès mémoire. Mais surtout, il est impossible de réutiliser une donnée chargée dans la pile, toute opération dépilant ses opérandes. Cela entraîne un grand nombre d'accès en mémoire RAM, défaut rédhibitoire quand la RAM est lente et peu chère et où la hiérarchie mémoire dicte sa loi. Les autres classes d'architectures n'ont pas ces défauts et font usage de modes d'adressage autres que le mode d'adressage implicite. L'usage de ces modes d'adressages permet d'éviter d'avoir à copier des données dans une pile, les empiler, et les déplacer avant de les manipuler. Le nombre d'accès à la mémoire est donc plus faible comparé à une machine à pile pure.
Les jeux d'instruction RISC vs CISC.
La seconde classification que nous allons aborder se base sur le nombre d'instructions. Elle classe nos processeurs en deux catégories :
Les processeurs CISC.
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" qu'on ne devait pas refaire à partir d'instructions plus simples, facilitait la vie des programmeurs.
Cette complexité des jeux d'instructions n'a pas que des avantages "humains", mais a aussi quelques avantages techniques. Le premier est une meilleure densité de code : un programme codé sur CISC utilise moins d'instructions et prend moins de mémoire. À l'époque des processeurs CISC, la mémoire était rare et chère, ce qui faisait que les ordinateurs n'avaient pas plusieurs gigaoctets de mémoire : économiser celle-ci était crucial. Cet avantage était donc crucial, ce qui contrebalançait les défauts de ces architectures. Ces défauts étaient essentiellement le fait que le grand nombre d'instructions entraîne une grande consommation de transistors et d'énergie. La difficulté de conception de ces processeur était aussi sans précédent.
Les processeurs RISC.
Est-ce que les instructions complexes des processeurs CISC sont vraiment utiles ? Pour le programmeur qui écrit ses programmes en assembleur, elle 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. Nombre de ces instructions ne sont utilisées que dans de rares cas, voire jamais. Autant dire que beaucoup de transistors étaient gâchés ! L'idée de créer des processeurs possédant des jeux d'instructions simples et contenant un nombre limité d'instructions très rapides commença à germer. Ces processeurs sont de nos jours appelés des processeurs RISC (acronyme de "Reduced Instruction Set Computer"). 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'une simplicité non-négligeable du processeur et une moindre consommation thermique/de transistors.
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.
Conclusion.
Durant longtemps, les CISC et les RISC eurent chacun leurs admirateurs et leurs détracteurs. Au final, on ne peut pas dire qu'un processeur CISC sera toujours meilleur qu'un RISC ou l'inverse et 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. Le CISC était bien plus utile quand on programmait encore en assembleur et que la mémoire était limitée. 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 transistors ou de simplicité de conception.
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 techniques et 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 techniques 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 quelques 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 marche de niche comme les supercalculateurs, etc.
Les jeux d'instructions spécialisés.
En parallèle de ces 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 :
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 souvent compilés vers un langage intermédiaire, avant d'être transformés en langage machine. Le langage intermédiaire qui sert...d'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 code machine associé à cette architecture est appelé le "bytecode". 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, on peut plus facilement créer 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 partie : 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. C'est aussi utilisé par certains langages de programamtion comme le Python ou le Java, afin d'obtenir une bonne compatibilité : on compile le Python/Java en "bytecode", qui lui-même est compilé ou interprété à l’exécution. Tout ordinateur sur lequel on a installé une machine virtuelle Java/Python peut alors exécuter ce "bytecode".
Le jeu d'instruction de la machine virtuelle varie grandement d'une machine virtuelle à l'autre, mais il est optimisé de manière à ce que la traduction en code machine soit la plus simple possible. Classiquement, la machine virtuelle est une machine à pile, pour diverses raisons. Premièrement, cela réduit la taille du "bytecode". Deuxièmement, il existe un algorithme assez simple et assez rapide qui traduit un code écrit pour une machine à pile en un code écrit pour une machine à registre. Et cet algorithme se débrouille pas trop mal pour attribuer les registres et les utiliser au mieux. Et il fonctionne assez bien peu importe le nombre de registres. C'est un avantage assez important pour les langages interprétés, où on n'utilise non pas un compilateur, mais un interpréteur. Sur les machines virtuelles récentes utilisées par les compilateurs, le nombre de registres n'est pas précisé, voire est infini. Cela déporte la phase d'allocation de registres (mettre telle valeur dans tel registre, gérer les échanges entre registres ou registres<->RAM) lors de la traduction en langage machine. Et cela permet de gérer des architectures très différentes qui n'ont pas les mêmes nombres de registres.
Si une machine virtuelle est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises. Et on peut l'implémenter en matériel ! le cas le plus étonnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre, avec un jeu d'instruction simple, une architecture à pile, etc. En temps normal, le "bytecode" Java n'est pas exécuté directement par le processeur, mais est compilé ou interprété. Et c'est 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. Les architectures de ce type permettent de se passer de l'étape de traduction "bytecode" -> langage machine, vu que leur langage machine est le "bytecode" Java lui-même.
Il existe d'autres cas un peu particuliers où une machine virtuelle a été implémentée en hardware. Par exemple, les concepteurs de langages fonctionnels apprécient notamment la machine SECD. Celle-ci a été implémentée en matériel par plusieurs équipes, par l'intermédiaire de micro-code. La première implémentation a été créée 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" utilisé comme représentation intermédiaire pour le langage FORTH.
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.
Les architectures à capacités.
D'autres processeurs incorporent des méthodes qui permettent d’améliorer la sûreté de fonctionnement ou la sécurité. Elles permettent d'éviter certaines attaques logicielles, comme des virus ou des accès non autorisés directement en matériel. Ces jeux d'instructions sont conçus en synergie avec certains systèmes d'exploitation. Sur les architectures à capacités, chaque adresse utilisée par un morceau de code se voit attribuer des autorisations d'accès, qui peuvent varier. Par exemple, le code d'un système d'exploitation aura accès en écriture à certaines adresses critiques, qui contiennent des paramètres de sécurité critique, mais les autres programmes n'y auront accès qu'en lecture (voire pas du tout). Les droits d'accès ne seront pas les mêmes, et les capacités différentes (parfois pour une même adresse). Ces droits d'accès sont rassemblés dans une suite de bits, que l'on appelle une "capability", ou capacité. Celles-ci décrivent :
Les instructions de lecture et écriture prennent comme argument une adresse et une capacité. Pour avoir accès à une adresse, le programme doit fournir automatiquement la capacité qui va avec : il doit la charger dans des registres spécialisés. Ces capacités sont mémorisées dans la mémoire RAM : chaque programme ou fonction a accès à une liste de capacités en mémoire RAM. Les instructions qui manipulent les registres de capacités ne peuvent pas, par construction, augmenter les droits d'accès d'une capacité : ils peuvent retirer des droits, pas en ajouter. Ce mécanisme interdit donc à tout sous-programme ou programme de modifier une adresse qui n'est pas dans sa liste de capacité : le programme ne pourra pas récupérer la capacité, et n'aura pas accès à l'adresse voulue. Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sûreté de fonctionnement indéniable. Du moins, c'est la théorie : tout repose sur l'intégrité des listes de capacités : si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
Un chapitre entier sera dédié à ces architectures, à la fin du livre.