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 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 : les 5 architectures canoniques
modifierLa première classification que nous allons voir est basée sur l'adressage des opérandes par les instructions arithmétiques. 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, idem pour les accès mémoire. Tel n'est pas le cas des instructions de calculs, qui ont souvent un nombre plus limité de modes d'adressage, du moins sur la plupart des architectures, qui limitent les modes d'adressages pour les opérandes afin de simplifier le processeur. Dans les grandes lignes, on trouve cinq catégories principales : les architectures mémoire-mémoire, les architectures à registres, les architectures LOAD-STORE, les architectures à accumulateur, les architectures à pile. Il s'agit de ce que je vais appeler les cinq architectures canoniques.
Les architectures à registres et LOAD-STORE
modifierLes architectures à registres peuvent stocker temporairement des données dans des registres généraux ou spécialisés. La présence de registre améliore grandement les performances comparé aux autres architectures. Le gain en performance est d'autant plus important que la mémoire est lente par rapport au processeur. Aussi, à part quelques architectures assez anciennes, toutes les architectures sont des architectures à registres. Cependant, les processeurs de ce type gèrent de nombreux modes d'adressages, et peuvent lire leurs opérandes soit depuis les registres, soit depuis la mémoire.
Une architecture à registre gère le cas où les deux opérandes sont dans des registres. Mais elle gère aussi au minimum un autre cas : celui où une opérande est lue en mémoire RAM/ROM, alors que l'autre est dans les registres. Les instructions de ce type sont appelées des instructions load-op. Les processeurs pouvant lire les deux opérandes depuis la mémoire sont rares. Par exemple, sur les processeurs x86, impossible d'avoir les deux opérandes lues depuis la mémoire. Il existe quelques rares processeurs qui permettent d'écrire le résultat d'une instruction directement en mémoire, sans passer par les registres. Et il peut y avoir des restrictions.
Les architectures LOAD-STORE se distinguent des architectures à registres sur un détail : les instructions arithmétiques et logiques ne peuvent pas lire leurs opérandes en mémoire, seules les instructions d'accès mémoire le peuvent. Leurs opérandes sont systématiquement dans les registres, il n'y a pas d'instructions load-op. En conséquence, les instructions de calcul 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.
Les architectures mémoire-mémoire
modifierLes 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 ces architectures, il n'y a pas de registres généraux : les instructions n'accèdent qu'à la mémoire principale. Malgré l'absence de registres pour les données, le program counter existe toujours, de même que le registre d'état et le pointeur de pile.
Le défaut des architectures mémoire-mémoire est qu'elles font beaucoup d'accès mémoire, du fait de l'absence de registres. Aussi, la performance dépendait grandement de la performance de la mémoire RAM. En conséquence, les processeurs de ce type étaient couplés à une mémoire assez rapide pour servir ce grand nombre d'accès mémoire. Au début de l'informatique, le processeur et la mémoire RAM avaient des performances similaires. Mais depuis que la mémoire est devenue très lente comparé au processeur, ce genre d'architectures est tombé en désuétude.
Un autre défaut de ces processeurs est que leurs instructions de calcul effectuent plusieurs accès mémoire. Prenons une instruction dyadique, à savoir qu'elle lit deux opérandes et calcule un résultat unique. Elle demande de faire trois accès mémoire : deux pour charger les opérandes, une pour enregistrer le résultat. Et il faut séquencer ces accès mémoire, à savoir faire deux lectures consécutives à deux adresses différentes pour charger les opérandes, pour ensuite écrire le résultat en mémoire. Ce séquençage des accès mémoire pour une seule instruction est assez complexe, demande de concevoir le séquenceur pour, et demande d'ajouter des registres internes au processeur qui sont cachés du programmeur.
Un cas intéressant d'architecture de ce genre est celle du processeur AT&T Hobbit. L'origine de ce processeur se trouve dans un projet de processeur abandonné par AT&T. Le processeur en question, la C-machine, était un processeur spécifiquement conçus pour le langage de programmation C. Et pour coller le plus possible à ce langage, le processeur n'utilisait pas de registres, seulement des adresses mémoires. Et vu que les programmes codés en C manipulent beaucoup la pile d'appel, le processeur avait une architecture mémoire-mémoire complétée par un cache de pile. Pour résumer, la pile d'appel est en mémoire, mais le sommet de la pile est stocké dans une mémoire LIFO intégrée au processeur. Le cache de pile contient 64 données de 4 octets chacun. En soi, il s'agit bien d'un cache, le processeur accède à la pile d'appel en mémoire RAM, mais ces accès sont interceptés par le cache de pile et c'est lui qui fournit la donnée demandée s'il la contient.
L'usage d'un cache de pile est assez spécifique aux architectures mémoire-mémoire, ainsi que pour les architectures à pile qu'on verra plus bas, même s'il doit exister quelques exceptions. La raison est que les architectures avec des registres utilisent des registres en lieu et place de ce cache de pile. De plus, les processeurs avec des registres utilisent généralement des mémoires caches générales, qui peuvent stocker n'importe quel type de données, et ne sont pas spécialisés pour la pile d'appel.
Les architectures à accumulateur
modifierLes architectures à accumulateur sont des architectures intermédiaires entre architectures à registre et architecture mémoire-mémoire. Elles visent à corriger un défaut des architectures mémoire-mémoire, en ajoutant un registre architectural unique. Plus haut, on a vu qu'une opération dyadique demande de faire trois accès mémoire : deux pour charger les opérandes, une pour enregistrer le résultat. Et cela demande d'ajouter de quoi séquencer ces accès mémoire, et d'ajouter des registres dans le processeur pour mémoriser les opérandes. Les architectures à accumulateur résolvent ce problème en ajoutant un registre unique, visible par le programmeur, afin d'éliminer des accès mémoire. Grâce à lui, une instruction dyadique ne fait qu'un seul accès mémoire, pas trois.
Elles incorporent un unique registre, appelé l'accumulateur, qui mémorise un opérande. Si l'instruction manipule plusieurs opérandes, les opérandes qui ne sont pas dans l'accumulateur sont lus depuis la mémoire RAM. De plus, le résultat d'une instruction est automatiquement mémorisé dans l'accumulateur. Grâce à ce registre, un opérande est lu depuis l'accumulateur, le résultat est stocké dans l'accumulateur, le seul accès mémoire à réaliser est celui pour la seconde opérande (si elle est nécessaire). Le nombre d'accès mémoire est donc grandement réduit pour les instructions dyadiques, les plus fréquentes.
Historiquement, les premières architectures à accumulateur ne contenaient aucun autre registre que 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és avec d'autres modes d'adressage, et lues en mémoire RAM. Le résultat ainsi qu'un des opérandes sont adressés de façon implicite car dans l'accumulateur, seule la seconde opérande étant adressée directement.
Les architectures à pile
modifierLes dernières architectures que nous allons voir sont les machines à pile, des jeux d'instructions où les opérandes sont organisés avec une pile semblable à la pile d'appel. De tels processeurs gèrent nativement une pile semblable à la pile d'appel, sauf qu'elle mémorise les opérandes des calculs et leur résultat. La pile est placée en mémoire RAM, on peut copier des opérandes dans la pile ou en retirer grâce à des instructions PUSH et POP qu'on analysera sous peu. Une instruction de calcul prend les opérandes qui sont au sommet de la pile. L'instruction dépile automatiquement les opérandes qu'elle utilise et empile son résultat.
Les opérandes sont ajoutés ou retirés de la pile grâce à deux instructions nommées PUSH et POP.
- L'instruction PUSH permet d'empiler une donnée. Elle prend l'adresse de la donnée à empiler, charge la donnée, et met à jour le pointeur de pile.
- L'instruction POP dépile la donnée au sommet de la pile, la stocke à l'adresse indiquée dans l'instruction, et met à jour le pointeur de pile.
Un défaut lié à l'absence des registres est qu'il est impossible de réutiliser une donnée chargée dans la 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. La solution a pour défaut que ces instructions sont des opérations à faire en plus, comparé aux autres architectures. Les autres classes d'architectures n'ont pas à copier des données dans une pile, les empiler, et les déplacer avant de les manipuler.
Les instructions de calcul manipulent les opérandes qui sont au sommet de la pile. Les opérandes sont retirés 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 permettent 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és 2, 3 ou 5 cases sous le sommet de la pile. Si les opérandes sélectionnés ne sont pas toujours retirés de la pile (tout dépend de l'architecture), le résultat est lui toujours placé au sommet de la pile. Ces jeux d'instruction n'utilisent pas la pile comme une mémoire LIFO, ce qui lui vaut le nom d'architecture à pseudo-pile.
Les machines à pile que je viens de décrire ne peuvent manipuler que des données sur la pile. Toutefois, des machines à pile plus évoluées ajoutent des modes d'adressages pour que la seconde opérande soit lue non pas depuis la pile, mais soit adressées explicitement en mémoire RAM, avec une adresse mémoire ou un mode d’adressage de type base+index. Notons que le résultat est stocké au sommet de la pile malgré tout. Les instructions pouvant adresser explicitement une ou plusieurs opérandes sont en plus des instructions normales qui lisent leurs opérandes sur la pile.
L'implémentation d'une architecture à pile est assez complexe, mais elle a l'avantage de ne pas utiliser beaucoup de registres. À part un program counter, une architecture à pile se contente d'un registre pointeur de pile, qui indique l'adresse du sommet de la pile en RAM. Il va de soi 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.
Pour de meilleures performances, tout ou partie de la pile est stockée directement dans le processeur, pour gagner en performance. En général, le sommet de la pile est stockée dans une mémoire tampon de type LIFO. Si jamais la LIFO en question est totalement remplie, le processeur peut gérer la situation de plusieurs manières. Avec la première, les données au bas de la pile débordent en mémoire RAM. Si la LIFO est déjà pleine au moment d'un PUSH, l'opérande au fond de la pile est alors envoyé en mémoire RAM. Reste qu'implémenter la gestion du débordement de la pile pile est quelque peu complexe. Une autre solution est de déléguer les débordements au logiciel. Un PUSH dans pune pile pleine déclenche une exception matérielle, dont la routine gère la situation.
Une autre solution remplace la LIFO par un cache de pile, qui mémorise les données au sommet de la pile. Il s'agit réellement d'un cache, dans le sens où il contient une copie du sommet de la pile d'appel, qui est aussi en RAM. La pile est donc en RAM, totalement, et seuls les portions utilisées de la pile sont maintenues dans le processeur. La gestion du débordement se fait en utilisant le remplacement des lignes de cache naturellement incorporé dans tout cache digne de ce nom.
La présence/absence de registres impacte le jeu d'instruction
modifierPour comprendre pourquoi les architectures à registre et LOAD-STORE dominent actuellement, il faut les comparer aux autres architectures canoniques. Et la différence principale concerne le nombre de registres. De ce point de vue, on peut distinguer trois classes d'architectures : celles sans registres de données, les architectures à accumulateur, et les architectures à registres.
Classe d'architecture | Nombre de registres pour les données |
---|---|
Architecture mémoire-mémoire | Aucun. |
Architecture à pile/à file | |
Architecture à accumulateur | Un registre appelé l'accumulateur. |
Architecture à registres | Plusieurs registres dits généraux et/ou spécialisés. |
Architecture LOAD-STORE |
La présence de registres réduit grandement le nombre d'accès mémoire à effectuer, ce qui améliore grandement la performance. Du moins, à condition que la mémoire soit moins rapide que le processeur, ce qui n'a pas toujours été le cas. Au début de l'informatique, mémoire et processeur étaient tout aussi rapides, ce qui fait que les architectures à registre ou LOAD-STORE n'avait pas d'avantage évident en termes de performances. Mais cela ne signifie pas qu'elles n'avaient ni avantages, ni inconvénients. La présence/absence des registres de données a de nombreuses conséquences assez intéressantes à étudier, bien au-delà des performances. Elle impacte le support des procédures, des interruptions, mais aussi la gestion des pointeurs/tableaux, ainsi que le support de certaines instructions mémoire.
La rapidité des interruptions/appels de fonction
modifierL'absence de registre a des avantages pour la gestion des procédures. Pas besoin de sauvegarder les registres du processeur à chaque appel de fonction, ni de les restaurer à la fin. La gestion de la pile est ainsi grandement simplifiée : pas de sauvegarde/restauration des registres signifie pas besoin d'instructions pour échanger des données entre registres et cadres de pile. Les programmes avec beaucoup d'appels de fonction économisent ainsi beaucoup d’instructions, ils étaient plus petits. L'avantage est d'autant plus important que les procédures/fonctions sont petites (une large partie des instructions est alors dédiée à la sauvegarde/restauration des registres) et nombreuses.
En conséquence, les architectures mémoire-mémoire et les architectures à pile ont un avantage pour ce qui est des appels de fonction. L'avantage concerne aussi les interruptions, qui sont des appels de fonction comme les autres. Ce qui fait que les architectures sans registres de données sont parfois utilisés comme processeurs pour gérer les entrées-sorties, vu que ces processeurs doivent fréquemment gérer des interruptions matérielles. Ils sont aussi très adaptés pour des processeurs faible performance dans l'embarqué, dans des cas d'utilisation où les interruptions matérielles sont fréquentes.
Les architectures à accumulateur sont aussi dans ce cas. Avec elles, il n'y a qu'un seul registre accumulateur en plus comparé aux architectures sans registres, ce qui ce qui rend la sauvegarde/restauration des registres très rapide. Et encore, dans quelques situations, l'accumulateur n'a pas à être sauvegardé. Et tout cela vaut aussi pour les interruptions. Aussi, beaucoup de processeurs embarqués à faible performance, qui doivent gérer un grand nombre d'entrées-sorties, sont des architectures à accumulateur. Leur simplicité de conception, leur facilité de programmation, leur parfaite adaptation aux interruptions fréquentes, les rend idéaux pour ce genre d'applications.
Les modes d'adressage indicés et indirects impliquent la présence de registres
modifierUn désavantage lié à l'absence des registres est lié au calcul des adresses. Par exemple, de nombreux modes d'adressage ne sont pas possibles dans registres pour les données/adresse. Prenez le mode d'adressage base + index, qui prend une adresse et y ajoute un indice, les deux étant stockés dans des registres séparés. Il n'est théoriquement pas possible sur les architectures mémoire-mémoire et les architectures à pile. De même, il n'est pas possible sur les architectures à accumulateur, vu qu'il n'y a pas assez de registres.
En clair, les modes d'adressage possibles sont très limités. Outre l'adressage implicite et l'adressage immédiat (constante inclue dans l'instruction), il ne reste que l'adressage absolu. Les modes d'adressage indirect à registre ne sont simplement pas possibles, de même que les modes d'adressage indicés (base + indice et variantes). Or, ces modes d'adressage sont très utiles pour manipuler des pointeurs et des tableaux. Sans ces modes d'adressages, l'utilisation de tableaux ou de structures de données était un véritable calvaire, qui se résolvait parfois à grand coup de code automodifiant.
Pour améliorer la situation, les processeurs à accumulateurs ont alors incorporé des registres d'indice, pour faciliter les calculs d'adresse mémoire. Les registres d'indice stockent des indices de tableaux. Les registres d'indice permettait de supporter le mode d'adressage Indexed Absolute (adresse fixe + indice variable). Les autres modes d'adressages, comme le mode d'adressage base + indice ou le mode d'adressage indirect à registre étaient difficiles à mettre en œuvre sur ce genre de machines. Il faut dire que l'on ne pouvait pas mémoriser de pointeur dans un registre d'indice.
Au départ, ces processeurs n'utilisaient qu'un seul registre d'Index qui se comportait comme un second accumulateur spécialisé dans les calculs d'adresses mémoire. Le processeur supportait de nouvelles instructions 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, les processeurs finirent par incorporer plusieurs de ces registres. Les instructions de lecture ou d'écriture devaient alors préciser quel registre d'indice utiliser, en précisant un nom de registre d'indice, un numéro de registre d'indice.
Un exemple est le cas du processeur Motorola 6809, un processeur à accumulateur qui contient deux registres d'indices nommés X et Y. L'accumulateur est noté D et fait 16 bits, il peut être parfois géré comme deux accumulateurs séparés A et B de 8 bits chacun. Il contenait aussi deux pointeurs de pile, l'un pour les programmes, l'autre pour le système d'exploitation, ainsi qu'un program counter. Le registre de page était utilisé pour l'adressage absolu, comme vu dans le chapitre sur les modes d'adressage.
Une autre solution est celle de l'instruction Index next instruction, que nous appellerons INI, qui a été utilisée sur des architectures comme l'Apollo Guidance Computer et quelques autres. Elle INI additionne une certaine valeur à l'instruction suivante. Par exemple, si l’instruction suivante est une instruction LOAD adresse 50, l'INI permet d'y ajouter la valeur 5, ce qui donne LOAD adresse 55. L'idée est d'émuler un adressage par indice : on utilise l'INI pour ajouter l'indice à l'instruction LOAD suivante. La valeur à ajouter est précisée via mode d'adressage absolu est lue depuis la mémoire. Un point important est que l'addition a lieu à l'intérieur du processeur, pas en mémoire RAM/ROM. Le mode d'adressage ne fait pas de code auto-modifiant, l'instruction modifiée reste la même qu'avant en mémoire RAM. Elle est altérée une fois chargée par le processeur, avant son exécution.
- Notons que cette instruction est aussi utilisée pour modifier des branchements : si l'instruction suivante est l'instruction JUMP à adresse 100, on peut la transformer en JUMP à adresse 150. Elle peut en théorie changer l'opcode d'une instruction, ce qui permet en théorie de faire des calculs différents suivant le résultat d'une condition. Mais ces cas d'utilisation étaient assez rares, ils étaient peu fréquents.
Les instructions d'accès mémoire LOAD et STORE
modifierUn point important est que toutes les architectures précédentes disposent d'instructions d'accès mémoire. Par contre, elles varient entre les 5 catégories précédentes.
Par exemple, la présence d'instruction LOAD et STORE n'est possible que sur les architectures disposant de registres, à savoir les architectures à accumulateur, à registre et LOAD-STORE. les autres architectures ont bien des instructions d'accès mémoire, mais pas d'instruction LOAD ni d'instruction STORE. Elles se contentent de quelques instructions mémoire, dont une instruction MOV pour copier une adresse dans une autre, une instruction XCHG pour échange le contenu de deux adresses, etc.
Les instructions LOAD et STORE existent bel et bien sur les architectures à accumulateur, mais n'ont pas les mêmes modes d'adressages. L'instruction LOAD copie une donnée de la RAM vers l'accumulateur, l'instruction STORE copie l'accumulateur dans une adresse. Les deux instructions n'ont pas besoin d'adresser l'accumulateur, qui est adressé de manière implicite, mais doivent préciser l'adresse dans laquelle écrire/lire.
Il faut aussi noter que les architectures LOAD-STORE sont les seules à avoir une stricte séparation entre instructions d'accès mémoire et instructions de calcul. Sur les autres architectures, la plupart des instructions peuvent aller chercher leurs opérandes en RAM, et effectuent dont des accès mémoire. 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. Mais avec les architectures LOAD-STORE, les accès mémoire sont circonscrits aux instructions d'accès mémoire, les instructions de calcul/branchement n’effectuent pas le moindre accès mémoire.
Classe d'architecture | LOAD et STORE | Séparation des instructions d'accès mémoire et de calcul |
---|---|---|
Architecture à pile | Non | Non |
Architecture mémoire-mémoire | ||
Architecture à accumulateur | Oui, accumulateur adressé implicitement | |
Architecture à registres | Oui, registres adressés explicitement | |
Architecture LOAD-STORE | Oui |
Si on analyse le nombre d'accès mémoire par instruction de calcul, on peut déceler d'autres différences entre les 5 architectures précédentes. La pire de ce point de vue est celui des architectures sans registres, où toutes les instructions lisent leurs opérandes en mémoire, et écrivent les résultats en mémoire RAM. C'est le cas sur les architectures mémoire-mémoire, mais aussi sur les architectures à pile ! Les architectures à accumulateur sont intermédiaires : la présence de l'accumulateur fait qu'on n'a qu'un seul accès mémoire par instruction dyadique, dans le meilleur des cas. Les architectures à registre réduisent encore le nombre d'accès mémoire par instruction, allant de zéro si tous les opérandes sont dans des registres, à un peu plus si un opérande est à lire dans la RAM. Les architectures LOAD-STORE ganratissent qu'il n'y a au maximum pas d'accès mémoire par instruction.
Classe d'architecture | Nombre d'accès mémoire minimum par opération dyadique | Nombre d'accès mémoire dans le pire des cas |
---|---|---|
Architecture à pile | Trois accès mémoire par opération : un par opérande, un pour le résultat | |
Architecture mémoire-mémoire | ||
Architecture à accumulateur | Un accès mémoire par instruction, pour lire la seconde opérande | Variable, dépend du jeu d'instruction et des modes d'adressages supportés |
Architecture à registres | Zéro si les opérandes sont dans les registres | |
Architecture LOAD-STORE | Zéro : les opérandes sont dans les registres |
La densité de code
modifierAu début de l'informatique, la mémoire avait des performances similaires à celles du processeur, mais elle était de petite taille. Les programmes gagnaient à être de petite taille. Aussi, les architectures se distinguaient sur autre chose : la densité de code. La densité de code n'est ni plus ni moins que la taille des programmes. Les mémoires de l'époque étaient assez petites, aussi il était avantageux d'avoir des programmes assez petits. La taille d'un programme dépend de deux choses : le nombre d'instructions, et la taille de celles-ci. Et ces deux paramètres sont influencés par l'architecture. La taille des instructions est surtout influencée par les modes d'adressage disponibles.
Les architectures 0, 1, 2 et 3 références
modifierPour ce qui est de comparer la densité de code des cinq architectures canoniques, il est intéressant de parler de la distinction entre architectures 0, 1, 2 et 3 adresses. Elle est lié aux opérations arithmétiques dites dyadiques, à savoir qui ont deux opérandes. Les plus communes sont les additions, multiplications, soustraction, division, etc. Les instructions de test sont aussi dans ce cas, pour la plupart.
Elles ont besoin de préciser trois choses : la localisation des deux opérandes, et l'endroit où ranger le résultat. Opérandes et résultat peuvent être adressés soit explicitement, soit implicitement. Par implicitement, on veut dire avec le mode d'adressage implicite, alors que explicite signifie qu'on doit préciser un numéro/nom de registre, une adresse mémoire, ou toute autre référence pour localiser l'opérande/résultat. Et le nombre de référence par instruction dyadique varie grandement suivant l'architecture utilisée.
Notons que ce qui nous intéresse ici est le cas des instructions dyadiques uniquement. La raison est que les autres instructions ont un encodage similaire. Par exemple, toutes les instructions d'accès mémoire utilisent généralement l'adressage absolu sur les 5 architectures canoniques, ce qui demande juste une adresse mémoire et un opcode. Les instructions d'accès mémoire PUSH et POP sont dans ce cas, pareil pour les instructions LOAD et STORE des des architectures LOAD-STORE, ou pour les instructions pour copier une donnée dans l'accumulateur ou inversement pour copier l'accumulateur dans une adresse. L'encodage des branchements est lui aussi globalement le même d'une architecture à l'autre.
Les architectures à zéro référence sont les architectures à pile. Elles n'ont pas besoin de préciser la localisation de leurs opérandes, qui sont au sommet de la pile et sont adressées implicitement, sauf pour Pop et Push Et sachez que cela vaut aussi pour les architectures à pile où il est possible de préciser l'adresse mémoire d'un opérande. De telles architectures supportent des instructions où tous les opérandes sont lus depuis la pile, et d'autres avec un opérande adressé explicitement et l'autre implicitement. Mais ce sont des instructions annexes, la majorité des instructions adressant tous les opérandes implicitement.
Les architectures à une référence correspondent aux architectures à accumulateur. Les instructions dyadiques lisent un opérande depuis la mémoire, la seconde est lue depuis l'accumulateur. Seule la première est adressée explicitement, alors que l'accumulateur est adressé implicitement. Le résultat de l'opération est enregistré dans l'accumulateur, là aussi avec un adressage implicite.
Les architectures à deux et trois références regroupent toutes les autres architecture.
Les architectures à deux références adressent les opérandes explicitement, mais le résultat est adressé implicitement. Pour ce faire, le résultat d'une instruction est stocké à l'endroit indiqué par la référence du premier opérande : l'opérande sera remplacé par le résultat de l'instruction. Avec cette organisation, les instructions ne précisent que deux opérandes, pas le résultat. Mais la gestion des instructions est moins souple, vu qu'un opérande est écrasé.
Les architectures à trois références permettent de préciser le registre de destination du résultat explicitement. Ce genre d'architectures permet une meilleure utilisation des registres, mais les instructions deviennent plus longues que sur les architectures à deux références.
La taille des instructions
modifierL'intérêt de cette distinction est une question de taille des instructions. La raison est assez intuitive : il faut encoder de quoi localiser un opérande adressé explicitement, à savoir encoder une référence. Plus elles sont nombreuses à être adressées explicitement, plus il faudra ajouter de bits à une instruction pour ça, plus les instructions sont longues. Mais il faut aussi tenir compte de la taille des références. Un numéro/nom de registre est bien plus court qu'une adresse, par exemple. Aussi, le nombre d'opérandes à adresser ne fait pas tout : il faut aussi tenir compte du mode d'adressage. Voyons ce qu'il en est pour chaque type d'architecture.
Avant toute chose, précisons que nous allons mettre de côté les instructions d'accès mémoire. La raison est qu'elles ont toutes des encodages et des tailles similaires, peu importe l'architecture. Elles se font presque toutes en adressage absolu, ce qui fait qu'elles encodent au minimum une adresse. Elles peuvent aussi préciser un registre sur les architectures à registre ou LOAD-STORE, mais cela ne change pas grand chose, leur taille est relativement similaire. Nous allons nous concentrer sur la taille des instructions dyadiques, qui sont généralement des instructions de calcul arithmétique ou logique.
Les architectures à zéro référence ont les instructions dyadiques les plus courtes, car elles adressent toutes leurs opérandes implicitement. Elles sont aussi appelées architectures à zéro adresse par abus de langage. Les instructions se limitent généralement à un opcode, du moins pour les instructions de calcul et les branchements.
Les architectures à une référence utilisent une adresse mémoire pour adresser l'opérande en RAM. Elles sont aussi appelées architectures à une adresse par abus de langage. Les instructions dyadiques utilisent donc un opcode couplé à une adresse mémoire. L'adresse mémoire est généralement assez longue, plus que l'opcode.
Les autres architectures sont toutes à deux ou trois références. Par contre, la nature de ces références change d'une architecture à l'autre.
- Sur les architectures LOAD-STORE, les instructions dyadiques utilisent des numéros de registres, mais aucune adresse. Aussi, encoder les 2/3 registres adressés prend autant, voire moins de place qu'une adresse. Elles ont donc les instructions les plus courtes des trois, leur instructions sont globalement de même taille que sur les architectures à accumulateur, voire un peu moins.
- Les architectures mémoire-mémoire encodent deux/trois adresses mémoires en plus de l'opcode. Elles ont les instructions les plus longues des trois.
- Les architectures à registre sont intermédiaires entre les deux cas précédents. Leurs instructions ont la même taille que sur une architecture LOAD-STORE dans le meilleur des cas, si opérandes et résultat vont dans les registres. Pour les instructions load-op, leur taille est légérement supérieure à celle d'une architecture à accumulateur, vu qu'elles encodent une adresse et un registre. Les architectures devant encoder deux ou trois adresses sont très rares.
Si on fait le résumé, le classement en termes de longueur d'instruction est le suivant, en partant des instructions les plus courtes vers les plus longues.
- Architectures à pile ;
- Architectures LOAD-STORE ;
- Architectures à registres ;
- Architectures à accumulateur ;
- Architectures mémoire-mémoire.
Le nombre d'instructions d'un programme
modifierLes développements précédents nous parlent de la taille des instructions, mais il faut aussi tenir compte du nombre d'instructions. La taille d'un programme est en effet égale à la taille moyenne d'une instruction multipliée par le nombre d'instructions de ce programme. Et ce nombre est fortement influencé par l'architecture du processeur. Les architectures à pile et mémoire-mémoire ne sont pas égales sur ce point, pareil pour les différents types d'architectures à registre.
La raison est que tout ce que nous avons dit plus haut ne vaut que pour les instructions dyadiques. Les instructions d'accès mémoire ou les branchements ont des encodages similaires sur toutes ces architectures, elles ont la même taille sur les 5 architectures canoniques. Mais il faut savoir que les 5 architectures canoniques ne sont pas égales niveau accès mémoire. Pour faire la même chose, le nombre d'instructions mémoire ne sera pas le même sur les 5 architectures canoniques. Et le nombre d'instructions mémoire peut compenser l'effet de la taille des instructions dyadiques. Si on a des instructions dyadiques très courtes, cela peut être compensé par des un plus grand nombre d'instructions d'accès mémoire. L'étude de la densité de code demande donc de regarder comment se manifeste ce compromis pour les 5 architectures canoniques.
Les machines à pile ont un nombre d'instructions par programme est pourtant plus élevé que sur les autres architectures, en grande aprtie à cause des instructions mémoire. Une bonne partie des instructions sont des instructions Pop et Push. De plus, les programmes utilisent souvent des instructions pour dupliquer/copier des données dans la pile, chose qui n'est pas nécessaire sur les autres architectures. La situation est légèrement améliorée sur les architectures à pile supportent des instructions qui permettent de préciser l'adresse d'un opérande, voire des deux. Mais pas de quoi sensiblement changer la donne.
Les architectures à registre ont en théorie un désavantage en termes de nombre d'instructions, lié à la gestion des procédures. Elles ont besoin de sauvegarder les registres lors d'un appel de fonction, et de les restaurer quand la fonction termine. Cela implique des échanges avec la mémoire réalisée avec des instructions LOAD-STORE. Plus les fonctions sont fréquentes et petites, plus le désavantage est important. Le nombre d'instruction augmente donc assez rapidement, mais reste en-deça de celui des architectures à pile.
Les architectures LOAD-STORE sont identiques aux architectures à registres, avec cependant un léger désavantage en plus. Pour l'illustrer, prenons un exemple où on veut faire un calcul entre un opérande un registre, un opérande en RAM, et stocker le résultat dans un registre. Comparons une architecture à registre et une architecture LOAD-STORE. Avec une architecture LOAD-STORE, l'opérande en RAM doit être copiée dans un registre avec une instruction LOAD, avant de faire des calculs dessus avec une instruction de calcul. Avec une architecture à registre, on n'a besoin que d'une seule instruction. L'instruction est plus longue, mais elle en remplace deux. Plus la situation est fréquente, plus l'avantage est pour l'architecture à registre.
Les architectures mémoire-mémoire sont celles qui ont le moins d'instructions par programme. Comme les architectures à accumulateur, elles se passent d'un paquet d'opérations nécessaires sur les autres machines. Pas besoin de sauvegarder/restaurer les registres lors d'un appel de fonction, la gestion de la pile d'appel est simplifiée. Les architectures à accumulateur sont proches des architectures mémoire-mémoire pour les mêmes raisons, avec cependant un léger désavantage : elles doivent sauvegarder l'accumulateur lors des appels de fonction, et le restaurer après. Mais le désavantage est vraiment mineur.
Pour résumer :
- Les architectures mémoire-mémoire utilisent moins d'instructions par programme que les autres, elles se contentent du strict minimum.
- Les architectures à pile utilisent en plus des instructions PUSH et POP, ainsi que d'autres instructions pour manipuler la pile.
- Les architectures à registre ajoutent des instructions de sauvegarde/restauration des registres lors d'un appel de fonction, idem pour les architectures LOAD-STORE.
- Les architectures LOAD-STORE utilisent en plus des instructions LOAD-STORE pour copier les opérandes dans les registres et écrire les résultats en RAM, mais seulement si besoin.
Instructions d'accès mémoire hors opérandes | Instructions pour gérer les opérandes/résultats | |
---|---|---|
Architecture mémoire-mémoire | ||
Architecture à pile | Instructions PUSH et POP, ainsi que d'autres instructions pour manipuler la pile. | |
Architecture à registres | Instructions de sauvegarde/restauration des registres lors d'un appel de fonction | |
Architecture LOAD-STORE | Instructions LOAD-STORE pour lire les opérandes et enregistrer les résultats. | |
Architecture à accumulateur | Rares copies de l'accumulateur en mémoire RAM. |
La conclusion : la densité de code finale
modifierSi on compare les 5 types d'architectures précédentes, on s’aperçoit que c'est surtout la taille des instructions qui compte en premier lieu, suivi par le nombre d'instruction, et enfin par le caractère 0/1/2/3 opérandes. Les machines à pile ont la meilleure densité de code, suivies par les architectures à registre, puis par les architectures LOAD-STORE, puis par les architectures à accumulateur, et enfin par les architectures mémoire-mémoire.
Les architectures à pile ont des programmes avec plus d'instructions, mais cela est plus que compensé par le fait que ce sont des architectures à zéro adresse, où les instructions sont très petites. Les architectures à registre ont une bonne densité de code, bien qu'inférieure à celle des architectures à pile, du fait que de la petite taille de leurs instructions dépasse l'effet des instructions liées aux appels de fonction. Les architectures LOAD-STORE viennent en troisième position, car leurs instructions sont aussi longues que celles à registres, mais qu'elles ont le désavantage des instructions LOAD-STORE. Enfin, les architectures sans registres viennent en dernier du fait de leur encodage désastreux, qui encodent des adresses mémoires. Les architectures à accumulateur sont en quatrième position, car ce sont des architectures 1-adresse, là où les architectures mémoire-mémoire sont de type 2/3 adresses.
Les jeux d'instruction RISC vs CISC
modifierLa seconde classification que nous allons aborder se base sur le nombre d'instructions. Elle classe nos 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".
Les différences entre CISC et RISC
modifierLa différence la plus intuitive est le nombre d'instructions supporté. Les processeurs CISC supportent beaucoup d'instructions, dont certaines assez complexes, alors que les processeurs RISC se contentent d'un petit nombre d'instructions de base assez simples. Les processeurs CISC incorporent souvent des instructions complexes, capables de remplacer des suites d'instructions simples. Par exemple, on peut implémenter des instructions pour le calcul de la division, de la racine carrée, de l'exponentielle, des puissances, des fonctions trigonométriques : cela évite d'avoir à simuler ces calculs à partir d'additions ou de multiplications. 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. Par contre, il n'est pas garanti que les instructions ajoutées sont plus rapides que leur équivalent logiciel, ce qui n'est pas gagné. L'avantage est surtout que le programme prend moins d'instructions pour faire la même chose, ce qui augmente la densité de code.
Une autre différence très liée à la précédente est une différence au niveau des modes d'adressages. Les processeurs CISC supportent plus de modes d'adressage que les processeurs RISC, mais ce n'est pas la différence principale. Les processeurs CISC sont techniquement des architectures à registres, alors que les processeurs RISC sont des architectures LOAD-STORE. Les processeurs CISC supportent au minimum les instructions load-op, où une opérande est lue en mémoire RAM/ROM. Plus rarement, les instructions des CPU CISC peuvent ainsi effectuer plusieurs accès mémoire par instruction, si plusieurs opérandes sont à lire en mémoire RAM.
La conséquence est la disponibilité des modes d'adressage. Sur les processeurs RISC, les instructions de calcul n'utilisent que le mode d'adressage inhérent (à registre), ce sont les instructions LOAD et STORE qui ont droit à des modes d'adressages plus ou moins élaborés (par adresse, base+index, autres). Mais sur les processeurs CISC, les modes d'adressage élaborés sont disponibles pour les instructions load-op et autres, pas seulement les instructions mémoire.
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 contraintes d'implémentation sont différentes
modifierLes 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
modifierLes 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
modifierEn 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 :
- 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
modifierCertains 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
modifierCertaines 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
modifierCertains 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
modifierCertains 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
modifierComme 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 : 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 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, 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 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 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 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 procédé d'interprétation est le suivant. Premièrement, le langage de haut niveau est traduit en bytecode, via un processus de pré-compilation. Puis, le bytecode est passé à un logiciel appelé l'interpréteur, qui lit une instruction à la fois dans le bytecode. L’interpréteur exécute alors 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 une machine à pile, pour diverses raisons. Premièrement, cela réduit la taille du bytecode. Deuxièmement, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour une machine à registre. Il fonctionne peu importe le nombre de registres, et 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. Et on peut l'implémenter en matériel ! Un premier exemple est celui des Pascal MicroEngine, des processeurs 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. 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.
Mais le cas le plus impressionnant 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.
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.