# Fonctionnement d'un ordinateur/Les architectures canoniques

Les architectures que nous avons vu précédemment dans ce cours disposent de plusieurs registres pour les données, en plus du pointeur de pile, d'un program counter, et de quelques autres. Elles sont appelées des architectures à registres, terme qui trahit bien le fait qu'elles ont des registres généraux ou spécialisés pour stocker temporairement des données. Et si on leur a donné un nom, c'est parce qu'il existe des architectures qui ne sont pas dans cette catégorie.
Les architectures peuvent se classer en quatre types : architectures mémoire-mémoire, architectures à accumulateur, architectures à pile et architectures à registres. Il s'agit de ce que je vais appeler les architectures canoniques. Elles se distinguent sur le nombre de registres, voire leur absence, et sur la manière dont sont gérées leurs opérandes. Le nombre de registres permet de distinguer trois classes d'architectures. Les architectures sans registres sont elles-mêmes subdivisées en architecture mémoire-mémoire et à pile, pour des raisons qu'on expliquera dans ce chapitre.
| Classe d'architecture | Nombre de registres pour les données |
| Architecture mémoire-mémoire | Aucun. |
| Architecture à pile | Aucun. |
| Architecture à accumulateur | Un registre appelé l'accumulateur. |
| Architecture à registres | Plusieurs registres dits généraux et/ou spécialisés. |
La présence de plusieurs registres améliore grandement les performances, comparé aux autres architectures. Aussi, à part quelques architectures assez anciennes, toutes les architectures récentes sont des architectures à registres. Les autres architectures canoniques ont été utilisées sur les tous premiers ordinateurs. Elles sont encore utilisées de nos jours sur des processeurs à base performance, typiquement dans l'embarqué.
L'absence/présence de registre a de nombreuses conséquences. La première est qu'elle impacte les instructions d'accès mémoire. Toutes les architectures précédentes disposent d'instructions d'accès mémoire, mais elles ne sont pas les mêmes selon l'architecture canonique considérée. Par exemple, les instructions LOAD et STORE n'existent que sur les architectures avec au moins un registre, à savoir les architectures à accumulateur et à registre. Les autres architectures n'ont pas d'instruction LOAD/STORE.
| Classe d'architecture | LOAD et STORE |
| Architecture à pile | Non |
| Architecture mémoire-mémoire | Non |
| Architecture à accumulateur | Oui, accumulateur adressé implicitement |
| Architecture à registres | Oui, registres adressés explicitement |
Une autre conséquence, assez liée à la précédente, est liée aux modes d'adressages supportés. Les architectures à registres disposent de leurs propres modes d'adressages spécifiques, qui ne sont pas supportés ailleurs. En pratique, les modes d'adressage pour les pointeurs sont radicalement différents. En l'absence de registres, il est possible d'implémenter des pointeurs, avec des modes d'adressages spéciaux qui n'existent pas sur les architectures à registres. Les architectures mémoire-mémoire ont leurs propres modes d'adressages, idem pour les machines à pile, idem pour les architectures à accumulateur. Maintenant que ces préliminaires ont été abordés, voyons dans le détail ces architectures canoniques.

## Les architectures mémoire-mémoire

Les toutes premières machines n'avaient pas de registres pour les données, pas de registres généraux. 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. Les instructions n'accédaient qu'à la mémoire RAM ou ROM : on parle d'architectures mémoire-mémoire.
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. Au début de l'informatique, le processeur et la mémoire RAM avaient des performances similaires, ce qui rendait ces architectures viables. 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. Une instruction dyadique fait trois accès mémoire : un par opérande, un pour enregistrer le résultat. Et il faut séquencer ces accès mémoire à l'intérieur de l'instruction, ce qui est complexe et demande d'ajouter des registres internes au processeur qui sont cachés du programmeur.

### La microarchitecture d'une architecture mémoire-mémoire

La microarchitecture d'une architecture mémoire-mémoire est assez simple. Elle contient une unité de calcul, des registres d’interfaçage avec la mémoire et une unité de contrôle. Elle n'a pas de registres, à part un program counter intégrés au séquenceur. La seule difficulté est d'incorporer de quoi gérer les opérations dyadiques et l'adressage mémoire indirect.
La première difficulté est que les opérations dyadiques lisent deux opérandes en RAM et écrivent le résultat en RAM. Mais le bus de donnée ne permet qu'une seule lecture ou écriture à la fois. Les accès mémoire doivent donc s'exécuter l'un après l'autre et doivent être séquencés. Les opérations mémoire-mémoire dyadiques s'exécutent donc en plusieurs micro-opérations. En effet, lire deux opérandes dans la mémoire se fait forcément en deux micro-opérations mémoire consécutives, sans compter les micro-opérations pour calculer le résultat et l'enregistrer en RAM.
Le séquenceur est conçu pour, mais il reste une difficulté : un seul registre d'interfaçage ne permet que le transfert d'un opérande. Lire une seconde opérande en RAM demande de rajouter un second registre d'interfaçage, qui est relié à l'autre entrée de l'ALU. Écrire le résultat en mémoire est géré en rajoutant un troisième registre d'interfaçage, relié à la sortie de l'ALU. Le séquenceur relie le registre d’interfaçage adéquat au bus mémoire à chaque étape d'une instruction.
La seconde difficulté est la gestion des pointeurs. Pour gérer les pointeurs, ces ordinateurs utilisent le mode d'adressage absolu indirect, absolument nécessaire sur des architectures. Avec ce mode d'adressage, l'instruction incorpore une adresse mémoire, mais ce n'est pas l'adresse de la donnée voulue : c'est l'adresse du pointeur, qui lui pointe vers la donnée. Il était systématiquement supporté sur toutes les architectures mémoire-mémoire. Elles supportaient aussi ses variantes à pré– ou post-incrément, où le pointeur était incrémenté/décrémenté automatiquement à chaque accès.
Avec ce mode d'adressage, La lecture de l'opérande se fait en deux étapes. Premièrement, le processeur extrait l'adresse de l'instruction et effectue une lecture pour récupérer le pointeur. Deuxièmement, la donnée lue, le pointeur, est envoyée sur le bus d'adresse pour récupérer la donnée. Le tout se fait en faisant communiquer les registres d’interfaçage de donnée et d'adresse, quelques multiplexeurs suffisent.

### L'exemple de l'ordinateur AT&T Hobbit

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.
Vu que les programmes codés en C manipulent beaucoup la pile d'appel, le processeur intégrait un cache de pile, spécialisé pour les accès à la pile d'appel. Il contenait 64 données de 4 octets. Il s'agit bien d'un cache, le processeur accède à la pile d'appel en mémoire RAM, mais le cache de pile en a une copie partielle et il intercepte les accès à la RAM, 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 et aux architectures à pile, même s'il doit exister quelques exceptions.

## Les architectures à accumulateur

Les architectures à accumulateurs sont apparues dès les débuts de l'informatique. Les tout premiers ordinateurs étaient de type mémoire-mémoire, puis les architectures à accumulateurs sont arrivées juste après, les deux ont existé conjointement pendant un temps.
A l'époque, les ordinateurs étaient gigantesques, ils prenaient une pièce de bâtiment entière dans le pire des cas, une armoire entière dans le meilleur. De plus, la technologie était telle que les registres étaient couteux en circuits et grignotaient rapidement le budget en transistor. Par contre, la mémoire RAM était assez rapide et était moins chère à produire. Les processeurs avaient donc intérêt à limiter le nombre de registres et à profiter d'une RAM rapide.
Les architectures mémoire-mémoire s’accommodaient des limitations techniques en n'utilisant pas de registres. Les architectures à accumulateur, elles, ajoutaient un registre unique afin de réduire le nombre d'accès mémoire. Le nombre d'accès mémoire était drastiquement réduit, passant de 3 par instruction, à seulement un. Le gain en performance était assez substantiel.

### L'accumulateur et le registre de multiplication

Les architectures à accumulateur sont centrées autour d'un registre architectural appelé l'accumulateur. Une opérande est lue depuis l'accumulateur, le résultat de l'instruction est automatiquement mémorisé dans l'accumulateur. En clair, il sert à la fois de source et de destination.
Il est utilisé pour toutes les opérations arithmétiques dyadiques, mais peut être utilisé pour les instructions monadiques. Pour les instructions monadiques, elles peuvent utiliser un opérande dans l'accumulateur, ou dans la mémoire RAM, les deux sont théoriquement possibles. Pour les instructions dyadiques, le premier opérande lu depuis l'accumulateur, le second opérande est lu depuis la mémoire RAM. En clair, toutes les instructions dyadiques sont de type load-op.
La conséquence est que le nombre d'accès mémoire est drastiquement diminué : de 3 par instructions sur une architecture mémoire-mémoire, on passe à seulement un avec un accumulateur. Les opérations dyadiques ont besoin d'un seul accès mémoire pour lire la seconde opérande, les opérations monadique en font aussi un seul pour lire leur unique opérande.
| Classe d'architecture | Nombre d'accès mémoire par opération dyadique |
| Architecture à pile | Trois accès mémoire par opération : un par opérande, un pour le résultat |
| Architecture mémoire-mémoire | Trois accès mémoire par opération : un par opérande, un pour le résultat |
| Architecture à accumulateur | Un accès mémoire par instruction, pour lire la seconde opérande |
| Architecture à registres | Zéro si les opérandes sont dans les registres, un pour les opérations load-op, LOAD et STORE. |
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. Grâce à l'accumulateur, une instruction ne fait qu'un seul accès mémoire maximum, ce qui rend le processeur très facile à implémenter.
Les ordinateurs étaient surtout utilisés pour du calcul scientifique ou des tâches d’ingénierie demandant beaucoup de calcul, rarement pour de la compatibilité. Les nombres flottants n'étaient pas encore apparus. A la place, les ordinateurs de l'époque géraient des nombres entiers de grande taille, de 30 bits ou plus. En conséquence, l'accumulateur faisait facilement 30 à 60 bits. Par exemple, les ordinateurs de la Série scientifique IBM 700/7000 géraient des entiers de 36 bits, l’accumulateur faisait 38 bits : 36 bits plus deux bits pour les débordements. Ce n'était pas spécifique aux architectures à accumulateur, les machines à pile de l'époque faisaient pareil.
Historiquement, les premières architectures à accumulateur ne contenaient aucun autre registre que l'accumulateur. Mais de nombreuses architectures à accumulateur ont ajouté un second registre pour les multiplication/divisions. Un exemple est celui de l'IBM 701, qui incorporait un registre accumulateur de 38 bits et un registre multiplieur de 36 bits. Le registre mémorise le multiplieur lors d'une opération de multiplication. Il mémorise donc un opérande, pas le résultat. Il peut aussi décaler le multiplieur vers la droite/gauche, ce qui est utile pour exécuter la multiplication. C'est ce qui est fait sur l'IBM 7094.
Une autre possibilité est que ce registre mémorise une partie du résultat de l'opération. Pour rappel, le résultat d'une multiplication/division est codé sur deux fois de bits que ses opérandes. Par exemple, multipliez deux opérandes de 30 bits, vous obtiendrez un résultat de 60 bits. Les 30 bits de poids faible du résultat vont dans l'accumulateur, les 30 bits de poids fort vont dans ce second registre.

### L'adressage indirect avec l'accumulateur et les registres d'indice

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, juste de préciser l'adresse à lire/écrire.
Les architectures à accumulateur supportaient souvent le mode d'adressage indirect mémoire. Un exemple est celui des ordinateurs Data General Nova, qui sont des architectures à accumulateur et qui supportaient ce mode d'adressage. Les deux instructions LOAD et STORE existaient en deux versions, distinguées par un bit d'indirection. Si ce bit est à 0 dans l'opcode, alors l'instruction utilise le mode d'adressage absolu normal : l'adresse intégrée dans l'instruction est celle de la donnée. Mais s'il est à 1, alors l'adresse intégrée dans l'instruction est celle du pointeur.
Cependant, la présence de l'accumulateur permettait d'utiliser l'adressage indirect à registre, pour gérer les pointeurs. Pour rappel, avec le mode d'adressage indirect à registre, l'adresse à lire/écrire est dans un registre. Ici, l'adresse à lire/écrire est prise dans l'accumulateur, seul registre disponible pour. L'adressage indirect est plus simple à implémenter pour l'instruction LOAD, car elle prend un seul opérande : l'adresse à lire. L'adresse à lire est placée dans l'accumulateur, la donnée lue est elle aussi chargée dans l'accumulateur. Pour l'instruction STORE, il faut fournir deux opérandes, l'adresse et la donnée à écrire, ce qui peut poser problème. Mais au pire, il est toujours possible d'utiliser l'adressage absolu ou indirect mémoire : l'adresse est dans l'instruction, la donnée à écrire dans l'accumulateur.
Les architectures à accumulateur incluent des optimisations pour les modes d'adressage base + indice et absolus indicés, qui prennent une adresse et y ajoute un indice. Pour cela, les processeurs à accumulateurs incorporent 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 permettaient de supporter deux modes d'adressage :
* Le mode d'adressage absolu indicé où une adresse fixe est additionnée à un indice variable. L'adresse fixe est intégrée dans l'instruction (adressage absolu), l'indice est dans un registre d'indice.
* Le mode d'adressage base + indice, où l'adresse est dans l'accumulateur et l'indice dans le registre d'indice.
Au départ, ces processeurs n'utilisaient qu'un seul registre d'indice qui se comportait comme un second accumulateur spécialisé dans les calculs d'adresses mémoire. Le processeur supportait de nouvelles instructions qui utilisaient ce registre d'indice 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.
Les processeurs à accumulateur supportaient souvent des variantes des modes d'adressage précédents, où le registre d'indice était automatiquement incrémenté ou décrémenté à chaque utilisation.
Il faut préciser que les registres d'indice ont une taille bien plus petite que l'accumulateur. Ils mémorisent des indices codés sur 16 bits ou moins, alors que l'accumulateur faisait typiquement 36 à 48 bits, parfois plus. La taille classique sur les anciens mainframes était de 15 bits, parfois moins. Il n'était pas rare de tomber sur des registres d'indice de 8 ou 9 bits. Leur petite taille rendait leur implémentation matérielle peu couteuse. Et c'est ce qui explique qu'ils étaient préférés à l'usage d'un second accumulateur.
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.
Les processeurs IBM avaient autrefois plusieurs registres d'indice. Par exemple, l'IBM 704 avait trois registres d'indice de 15 bits. Leur contenu était soustrait de l'adresse absolue. Une instruction pouvait utiliser plusieurs registres d'indice pour adresser son opérande. Toute instruction utilisait trois bits pour sélectionner les registres d'indice utilisés. Fait assez original, lorsque plusieurs registres d'indice sont sélectionnés, le processeur n'additionne pas les trois registres à l'adresse de base. À la place, il fait un OU bit à bit entre les registres d'indice sélectionnés, et additionne le résultat à l'adresse.
Les processeurs IBM à accumulateur ont fait ça sur toute la gamme IBM 700/7000, pour des raisons de compatibilité. Les processeurs suivants avaient 7 registres d'indices, mais conservaient les trois bits pour adresser les registres d'indices. Ils pouvaient fonctionner dans deux modes. Le premier mode est plus intuitif : les trois bits précisent un registre d'indice parmi les 7, qui est additionné avec l'adresse absolue. La valeur zéro indique qu'aucun registre d'indice n'est utilisé, ce qui explique qu'il y a 7 registres d'indice et non 8. Un second mode, compatible avec l'IBM 704, fait un OU logique entre les registres d'indice sélectionnés. Seuls les trois premiers registres d'indices peuvent être sélectionnés dans ce mode. Il y a deux instructions pour changer de mode : une pour passer en mode compatible, l'autre pour le quitter pour l'autre mode.
Quelques rares processeurs à registres généraux ont utilisés des registres d'indice, en plus des registres généraux. Ce qui fait l'association que nous avons faite entre registres d'indice et architectures à accumulateur est imparfait, bien que solide sur le principe. Un exemple d'architecture de ce type sont les architectures de la série UNIVAC 1100/2200. Ils disposaient de 128 registres, qui étaient mappés en mémoire à partir de l'adresse mémoire 0, la majorité étant inaccessibles par le programmeur. Ils regroupaient 12 registres accumulateurs, 11 registres d'indice et 4 registres hybrides qui pouvaient servir soit de registre d'indice, soit de registres accumulateurs.

### La microarchitecture des architectures à accumulateur

L'organisation interne d'une architecture à accumulateur est très différente de celle des processeurs à registre. Elle est plus ou moins la même pour tous ces processeurs, le point important étant que le chemin de données se résume à une ALU, un registre accumulateur et le bus de données. L'ALU est reliée au registre accumulateur, ainsi qu'au bus de données pour lire la seconde opérande, comme illustré ci-dessous. Il faut aussi gérer le cas des opérations LOAD/STORE et des modes d'adressages associés, mais c'est le séquenceur qui s'en occupe.
Pour simplifier l'implémentation, la seconde opérande est mémorisée dans un registre en amont de l'ALU. Par exemple, un registre d’interfaçage mémoire ou équivalent. C'était notamment très utile si le processeur utilisait une ALU plus courte que les opérandes. La grande taille des opérandes avait des conséquences sur l'unité de calcul utilisée, ainsi que sur la communication avec la mémoire. Il arrivait que de tels processeurs utilisaient des ALU sérielles, ou du moins octet-sérielles, plutôt qu'une véritable ALU 36 bits.
Avec l'adressage absolu, les instructions LOAD et STORE ne font que connecter l'accumulateur au bus de données. L'instruction STORE nécessite de connecter l'accumulateur au bus de données, de manière à ce que le transfert des données se fasse de l'accumulateur vers le bus de données. L'instruction LOAD s'implémente de la même manière, sauf que le sens de transfert est inversé. Cependant, il existe une optimisation qui permet d'implémenter l'instruction LOAD sans ajouter de circuits. Pour cela, il suffit que l'unité de calcul gére les opérations pass through, à savoir des opérations qui se contentent de recopier un opérande sur la sortie. Ici, l'idée est de recopier l'opérande provenant du bus de données.
La présence de registres d'indice est assez simple si l'architecture se limite au mode d'adressage absolu indicé. Pour rappel, avec celui-ci, l'adresse fournie par l'instruction est additionné avec un indice. L'indice est stocké dans des registres d'indice spécialisés, regroupés ans un banc de registre dédié. L'accumulateur est à part du banc de registre, il est toujours relié directement à l'ALU.
Deux solutions sont possibles : une ALU dédiée aux calculs d'adresse, une seule ALU pour toutes les opérations. Dans le premier cas, il y a une ALU séparée associée aux registres d'indice. L'ALU et les registres d'indice sont placés en sortie du séquenceur, en-dehors du chemin de données. La sortie de l'ALU est directement connectée au bus d'adresse.
En ajoutant un multiplexeur, on peut gérer nativement l'adressage base + indice. Dans ce cas, l'accumulateur doit être connecté au bus d'adresse. Typiquement, l'accumulateur est copié dans le registre d’interfaçage pour les adresses, puis la lecture s'effectue normalement. Au final, la donnée lue est copiée dans l'accumulateur. Si on veut gérer l'ajout d'un indice à l'accumulateur, le MUX est suivi par un circuit regroupant l'unité de calcul d'adresse et les registres d'indice. Le multiplexeur choisit quelle est l'adresse de base, l'addition se fait après.
Une autre option utilise un bus interne relié à l'accumulateur, l'unité de calcul, le banc de registre d'indice, et le bus de données. Avec cette solution, l'ALU est utilisé à la fois pour calculer les adresses et pour les instructions de calcul. De plus, en reliant le bus interne au bus d'adresse, on gère naturellement les adressages indirects. Le banc de registre est monoport, car on a besoin de lire ou d'écrire un indice à la fois.
Un exemple de processeur de ce genre est les processeurs Intel 4004 et 4040. Le processeur avait un banc de registre dédié aux registres d'indice, ainsi qu'un banc de registre pour le program counter et les pointeurs de pile. L'ALU servait pour les instructions de calcul, mais elle servait aussi à incrémenter le program counter, à incrémenter/décrémenter le pointeur de pile, et pour les calculs d'adresse avec adressage indicé.

## Les architectures hybrides registres-accumulateur

Il a existé quelques architectures qui étaient des hybrides entre architectures à accumulateur et architecture à registre. Elles ont servi de transition entre les deux, durant les années 80-90. Leur jeu d'instruction et leur microarchitecture était intermédiaire entre architecture à accumulateur et à registre. Il y en avait deux sous-types : celles qui utilisaient deux accumulateurs, celles qui couplaient un accumulateur avec plusieurs registres généraux. Nous appelerons les premiers des processeurs bi-accumulateurs, les seconds des processeurs registre-accumulateur.

### Les processeurs bi-accumulateurs : le Motorola 6800

Le Motorola 6800 était un processeur 8 bits qui gérait des adresses de 16 bits. Il contenait deux accumulateurs nommés A et B, de 8 bits chacun. Il disposait aussi d'un registre d'indice de 16 bits, d'un pointeur de pile de 16 bits, et d'un program coutner, lui aussi de 16 bits. Le registre d'état regroupait 6 bits, qu'on ne détaillera pas ici.
La présence des deux accumulateurs impactait surtout les instructions dyadiques. Les instructions monadiques étaient peu touchées, elles fonctionnaient normalement. Elles avaient juste à préciser où se trouvait l'opérande : soit en RAM, soit dans le premier accumulateur, soit dans le second. Le mode d'adressage permettait de préciser la localisation de l'opérande unique, à savoir soit l'adresse mémoire, soit l'un des deux accumulateurs.
Pour les opérations dyadiques, la gestion était totalement différente. En théorie, le premier opérande est dans un accumulateur, soit A, soit B. La seconde opérande vient soit de l'autre accumulateur, soit de la mémoire RAM. Et selon les instructions, tout variait.
Les opérations logiques permettaient de faire un ET/OU/XOR entre un accumulateur et un opérande venant de la mémoire. Il y avait une instruction par accumulateur. Par exemple, l'instruction ANDA faisait un ET entre l’accumulateur A et l'opérande mémoire, l'instruction ANDB faisait pareil, mais avec l'accumulateur B. Idem avec les opérations OU (instructions ORAA et ORAB) et XOR (instructions EORA et EORB). Il n'y avait pas de possibilité de prendre les deux accumulateurs comme opérande. A l'exception de l'addition, les autres instructions arithmétiques fonctionnaient sur le même modèle : un opérande venant de la mémoire, l'autre d'un des accumulateurs.
Pour l'addition, il y avait trois instructions. Les deux premières faisaient comme pour les opérations logiques : elles additionnaient un opérande provenant de la mémoire avec un accumulateur. L'instruction ADDA sélectionnait le premier accumulateur, l'instruction ADDB sélectionnait le second accumulateur. Mais une troisième instruction, l’instruction ABA, permettait d'additionner le contenu des deux accumulateurs.

### Les processeurs registres-accumulateur

Le deuxième type d'hybride accumulateur-registre avait un accumulateur unique couplé à un banc de registres secondaires. Sur ces processeurs, les instructions dyadiques devaient mettre leur premier opérande dans l'accumulateur, la seconde provenait soit des registres, soit de la RAM, soit d'une constante immédiate. Les opérations monadiques pouvaient lire leur opérande depuis l'accumulateur, les autres registres ou la RAM, tout était permis. Les registres d'indices disparaissaient sur ces architectures, en théorie. Les registres généraux pouvaient être utilisés comme registre d'indice ou pour stocker des opérandes, ils étaient assez versatiles pour servir de registres d'indices.
La microarchitecture de ces processeurs est très similaire à celle d'un processeur avec des registres d'indices. Plus précisément, avec un processeur avec un bus interne, relié à l'unité de calcul, au banc de registres et au bus de données/adresse. La seule différence est que le banc de registre contient des registres généraux et non des registres d'indices. Le banc de registre était monoport, car n'avait aucune raison d'être multiport. Pour les instructions dyadiques, seules à lire deux opérandes, il ne servait que pour la seconde opérande.
Sur les architectures Von Neumann, le bus interne au processeur est utilisé pour la lecture des instructions. Il est donc relié sur l'entrée du séquenceur. S'il y a un registre d'instruction, il est connecté au bus interne.
Un exemple ce processeur de ce type est celui des premiers processeurs Intel, des processeurs 8 bits, à savoir le 8008 et le 8080. Les processeurs Intel 8 bits avaient plusieurs registres : l'accumulateur A, et six registres nommés B, C, D, E, H et L. Les six registres sont regroupés dans un banc de registre à part de l'accumulateur.
Vu que les adresses font 16 bits, l'accumulateur 8 bits ne peut pas être utilisé pour l'adressage indirect. La solution a été de concaténer les deux registres H et L pour donner une adresse de 16 bits. Une conséquence est que le banc de registre avait deux ports : un pour le relier à l'accumulateur et au bus de données, l'autre pour le connecter au bus d'adresse. Afin de profiter au mieux de ce port d'adresse, le program counter et les pointeurs de pile étaient dans le même banc de registre.
Le Z80 est un processeur inspiré du design des processuers Intel 8 bits, mais avec pas mal d'améliorations. Le processeur Z80 avait un registre accumulateur nommé A, six registres de données nommés B, C, D, E, H et L, un registre d'état F. Le processeur Z80 avait des registres séparés ou les interruptions. Tous les registres précédents avaient une copie séparée accessible uniquement pour les interruptions, via fenêtrage de registres. Les registres pour interruptions sont nommés A', B', C', D', E', H', L', F'. En plus des registres précédents, le Z80 incorporait un pointeur de pile SP et deux registres d'indice nommés X et Y, qui n'étaient pas dupliqués pour les interruptions.
Le Z80 incorporait des instructions pour échanger le contenu des deux ensembles de registres, accumulateur inclus. L'instruction EXX échangeait le contenu des registres B, C, D, E, H et L et B', C', D', E', H' et L'. L'instruction EX échangeait les registres A,F avec les registres A',F'. Ces instructions permettaient d'utiliser les registres d'interruption pour les programmes et réciproquement. Cela permettait de doubler le nombre de registres pour les programmes, si les interruptions n'étaient pas utilisées.
A l'intérieur du processeur, tous les registres étaient regroupés dans un banc de registre unique, sauf les deux accumulateurs qui étaient séparés des autres registres. Pour l'accumulateur et le registre d'état, le choix entre registre normal et registre d'interruption se faisait par une bascule appelée la bascule A. Une bascule similaire était utilisée pour le banc de registres, appelons-la la bascule INT. Les instructions d'échange de registres du Z80 ne faisaient pas de copies, elles ne faisaient que modifier les bascules INT et A. L'instruction EX inverse la bascule A, l'instruction EXX inverse le contenu de la bascule I.
Il est aussi possible d'échanger le contenu des registres DE et HL avec une instruction dédiée, ce qui est là encore fait par deux bascules : une par ensemble de registres.

## Les architectures à pile

Les dernières architectures que nous allons voir sont les architectures à pile, aussi appelées machines à pile. Leur nom trahit le fait qu'elles gèrent nativement une pile semblable à la pile d'appel, sauf qu'elle mémorise les opérandes des calculs et leur résultat. 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. Le processeur peut copier des opérandes dans la pile ou en retirer grâce à des instructions PUSH et POP qu'on analysera sous peu.

### La séparation entre pile d'opérande et pile d'appel

Sur les machines à pile, il faut distinguer la pile d'appel et la pile d'opérande. La première est la pile pour les adresses de retour des fonctions, l'autre est une pile séparée utilisée pour les opérandes des instructions.
Il a existé des machines à pile avec une pile unique, qui servait à la fois de pile d'opérande et de pile d'appel. Elles étaient assez peu performantes. Mais la plupart des designs existants utilisent deux piles séparées, complètement distinctes. En clair, la pile d'appel est gérée à part de la pile d'opérande. La pile d'opérande est en mémoire RAM, sauf optimisation, pas forcément la pile d'appel. La pile d'appel est soit placée dans une mémoire séparée, soit placée dans la mémoire RAM principale. Elle peut être intégrée au processeur, bien que ce soit un cas assez rare.
Le cas le plus simple à étudier est celui d'une pile d'appel ayant sa propre mémoire, séparée de la mémoire pour les opérandes. La pile d'appel mémorise alors uniquement les adresses de retour pour les fonctions. Les deux piles n'ont pas forcément la même taille, à savoir que la pile d'appel est souvent plus petite que la pile des opérandes. Les appels de fonction, même si nombreux, utilisent assez peu de mémoire pour les adresses de retour. Vu qu'il y a deux piles, il y a deux pointeurs de pile. Un pointeur de pile pour la pile d'appel, un autre pour la pile des opérandes. Et vu qu'elles n'ont pas la même taille, les deux pointeurs n'ont pas forcément la même taille eux aussi. Ils sont gérés à par par le séquenceur.

### La pile d'opérande : instructions et modes d'adressage

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. Mais 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.
| Instruction Dup. | Instruction Swap. |
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 des opérandes dans la pile. Une instruction peut 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 instructions load-op, qui lisent la seconde opérande en mémoire RAM, avec une adresse mémoire ou un mode d'adressage pour les pointeurs. L'avantage est que la seconde opérande n'a pas à être placée sur la pile pour être utilisée. Mais de telles instructions sont assez peu utilisées.
Sur les machines à pile, l'adressage indirect utilise le sommet de la pile. L'adresse à lire est placée au sommet de la pile, elle est envoyée sur le bus d'adresse. Pour l'instruction STORE indirecte, la donnée à écrire est placée sous l'adresse, donnée et adresse sont dépilées par l'instruction STORE. Pour l'instruction LOAD indirecte, l'adresse à lire est dépilée au sommet de la pile, la donnée lue est empilée au sommet de la pile. Il est possible d'implémenter un adressage de type base + indice, il suffit de placer l'adresse de base et l'indice au sommet de la pile.

### Les mémoires reliées à une machine à pile

L'implémentation d'une architecture à pile n'est pas la même si une seule pile est utilisée, ou si la pile d'appel est séparée de la pile d'opérande. Ajoutons que les machines à pile peuvent être de type Harvard ou Von Neumann. La pile d'appel contient des copies passées sur program counter, ce qui fait que la pile d'appel est alimentée par le séquenceur, directement.
Enfin, il arrive que les machines à pile utilisent deux RAM séparées : une pour la pile d'opérande et une pour les structures de données complexes, comme les tableaux. La première est une mémoire LIFO digne de ce nom, l'autre est une mémoire RAM tout ce qu'il y a de plus normale. La mémoire RAM pour les tableaux était accédée via des modes d'adressage spéciaux, les instructions de calcul pouvaient préciser l'adresse d'une donnée dans la RAM. Et les instructions LOAD et STORE pouvaient copier une donnée de la RAM vers la pile d'opérande ou inversement. Les architectures de ce type avaient souvent des registres d'indices pour faciliter les calculs d'adresse. Un exemple d'architecture de ce type était celui des mainframes Burroughs Large Systems.
Dans le meilleur des cas, une seule mémoire est utilisée pour les deux piles et le programme. Il y a des versions où la pile d'appel est intégrée dans le séquenceur, laissant la mémoire pour le programme et la pile d'opérande. Il faut dire que la pile d'appel ne contient que des adresses de retour, elle est assez petite. Assez pour être intégrée dans le processeur.

### La microarchitecture d'une machine à pile

Dans ce qui va suivre, nous allons supposer que la pile d'appel est intégrée au séquenceur. Les architectures à pile de ce type peuvent être implémentées de plusieurs manières, la plus simple utilisant une architecture mémoire-mémoire améliorée, la plus complexe utilisant une architecture à registres sous-jacente.
La première implémentation utilise une architecture mémoire-mémoire sous-jacente, toute la difficulté de l'implémentation étant déportée dans le séquenceur. La pile d'opérande est placée en mémoire RAM, les instructions lisent leurs opérandes en RAM et empilent le résultat en RAM. Les instructions PUSH et POP font des copies en mémoire RAM pour empiler et dépiler les données. Pour cela, elles passent par l'intermédiaire d'un registre d’interfaçage, comme sur les architectures mémoire-mémoire.
Le séquenceur est adapté de manière à gérer une pile d'opérande, les instructions PUSH/POP/autres, et quelques autres détails. Le pointeur de pile est géré par le séquenceur, au même titre que les registres d'indice. Il est stocké à côté ou dans le séquenceur, souvent à côté du program counter, et il est incrémenté/décrémenté par l'unité de calcul d'adresse. Placer le pointeur de pile dans le séquenceur n'est pas spécifique des architectures à pile. De nombreuses architectures CISC faisaient pareil. Mais nous détaillerons cela dans le prochain chapitre, dans la section sur le program counter.
Une telle implémentation intègre souvent 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 seules 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.
Une autre implémentation ajoute un registre pour mémoriser l'opérande au sommet de la pile. Le registre agit comme un registre accumulateur. En effet, le sommet de la pile est le dernier résultat calculé par une instruction. De plus, il sera utilisé comme opérande de la prochaine opération. Du moins, tout cela est valable tant qu'on n'utilise pas d'instruction DUP ou SWAP, qui changent la position des opérandes dans la pile. L'implémentation n'est ni plus ni moins qu'une architecture à accumulateur au séquenceur modifié.
Une dernière implémentation place tout ou partie de la pile dans le processeur, pas seulement le sommet de la pile. Pour cela, elles utilisent une architecture registre-accumulateur améliorée, où le banc de registre est remplacé par une mémoire LIFO. Le sommet de la pile est stocké dans un registre accumulateur, le reste de la pile est dans une mémoire tampon de type LIFO, construite en améliorant un banc de registre. La mémoire LIFO ne permet de lire qu'un seul opérande, le second opérande situé sous le sommet de la pile.
    Il est techniquement possible d'implémenter la LIFO avec deux ports de lecture, ce qui fait que l'intérieur du processeur ressemble à une architecture LOAD-STORE avec des circuits en plus. Mais de telles implémentations sont rares.
Si jamais la LIFO 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 est quelque peu complexe. Une autre solution est de déléguer les débordements au logiciel. Un PUSH dans une pile pleine déclenche une exception matérielle, dont la routine gère la situation.

### Exemple : la Harris RTX 2000

Une architecture à pile assez réputée est la NC4016. Elle avait comme particularité d'être connectée à trois mémoires : une pour la pile d'appel, une autre pour la pile d'opérande, une autre pour la mémoire programme (architecture Harvard). Son successeur est la Harris RTX 2000, une ancienne architecture à pile qui a eu un petit succès dans les cours d'architecture des ordinateurs.
Elle disposait de deux piles intégrées au processeur : une pile d'appel de 256 adresses de retour et une LIFO de 256 opérandes. Les deux piles n'avaient aucune protection en cas de débordement, à savoir pour vérifier si elles sont pleines ou non. La pile était donc intégrée dans le processeur, qui était en réalité un microcontrôleur. C’était une architectures Harvard, avec un bus relié à une mémoire pour le programme, et un G-bus pour les périphériques.
Sa microarchitecture est illustré ci-contre. Rassurez-vous, nous n’allons pas l'étudier en détail, pour une raison simple : le grand nombre d'interconnexions rend son étude très compliquée. Remarquons qu'il y a deux bus internes : un bus Y et un bus T. Le bus Y est connecté sur une entrée de l'ALU et on comprend facilement qu'il récupère la seconde opérande pour une instruction de calcul. Le bus T, quant à lui, prend en entrée le résultat et l'envoie à destination.
Les registres sont mine de rien assez nombreux. En premier lieu, on trouve le program counter noté PC et le registre d'instruction noté IR. Le sommet de la pile est mémorisé dans un registre dédié. Mais on trouve aussi des registres MD, SR et CR dédiés à des opérations spécifiques. Le registre MD est utilisé pour les multiplications, afin de mémoriser la seconde opérande. Le registre SR est utilisé pour le calcul de la racine carrée, car le processeur supporte cette opération dans son jeu d'instruction. Il y avait aussi un registre d'indice I.
Niveau circuits de calcul, il y avait une unité de calcul assez complexe, un circuit incrémenteur pour le porgram counter et un circuit décrémenteur pour gérer les pointeurs de pile.

### Le support de plusieurs piles pour la multiprogrammation

Les ordinateurs Burroughs Large Systems étaient des machines à piles optimisées pour exécuter plusieurs programmes les uns à la suite des autres. C'est plus ou moins ce qui est fait sur les ordinateurs modernes, quand on veut exécuter plus de programmes qu'on a de coeurs. On change de programme régulièrement, chaque programme est interrompu pour laisser la place à un autre, et ainsi de suite. C'est le système d'exploitation qui se charge de changer d'un programme à un autre.
Pour cela, ils géraient nativement plusieurs piles, une par programme en cours d'exécution. Une pile fusionnait pile d'appel et pile d'opérande, elle servait les besoins pour un programme. De plus, ces processeurs avaient des instructions dédiées pour changer d'un programme à un autre.

## Les architectures à pile de registres

Plus haut, nous avons parlé des architectures à accumulateur, et vu qu'il existait des architectures hybrides accumulateur-registres. Quelque chose d'équivalent existe pour les machines à pile. Il s'agit de processeurs qui ont plusieurs registres, mais les utilisent comme une pile d'opérande. En clair, de tels processeurs ont une pile d'opérande intégrée dans le processeur, de petite taille, typiquement 3 à 16 registres maximum. La pile de registres est gérée par le programmeur, qui a conscience qu'il y a une RAM séparée de la pile d'opérande intra-CPU.

### L'extension flottante x87 des CPU x86

Un bon exemple est celui de l'extension x87 qui a ajouté les nombres flottants aux processeurs x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps. Les ajouts en question sont appelés des extensions x86. Nous allons maintenant voir celle qui a ajouté la gestion des flottants au x86. L'extension x87 n'est plus utilisée depuis l'arrivée des CPU 64 bits, car elle a été remplacée par l'extension SSE.
Sur les tout premiers processeurs x86, le support des nombres flottants n'était pas implémenté. A la place, ils utilisaient des co-processeurs arithmétiques, appelés des coprocesseurs x87. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Le premier coprocesseur de ce type était le 8087. Il a été suivi par le 187, le 287, le 387, le 487 et le 587. Ils étaient capables d'exécuter les 4 opérations de base (add, sub, mul, div), la racine carrée, des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS), les opérations trigonométriques sinus, cosinus et tangente, l'arctangente, et des instructions de calcul de logarithmes ou d'exponentielles.
Les coprocesseurs arithmétiques x87 avaient 8 registres flottants, qui étaient gérés avec une pseudo-pile, ainsi que 3 registres de contrôle. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'une opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus.
Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand.
| Pseudo-pile x87 - chargement d'une opérande. | Pseudo-pile x87 - retrait d'une opérande. |
Les instructions à une opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur.
Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire.
| Instruction | Description |
| FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |
| FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |
| FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |
En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres.
Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé Tag Word. Le registre Tag Word indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Le registre tag word fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé.
* Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ;
* Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ;
* Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ;
* Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant.
Le processeur x87 contient aussi deux registres d'état, nommés Control Word et Status Word. Le registre Status Word contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. Le registre Control Word fait 16 bits et configure la gestion des arrondis.

## Les avantages de l'absence de registres

Pour comprendre pourquoi les architectures à registre 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 | Aucun. |
| Architecture à accumulateur | Un registre appelé l'accumulateur. |
| Architecture à registres | Plusieurs registres dits généraux et/ou spécialisés. |
La présence de registres réduit grandement le nombre d'accès mémoire à effectuer, ce qui améliore grandement la performance. Un bon moyen de s'en rendre compte est de regarder le nombre d'accès mémoire par instruction. Il est d'autant plus élevé en absence de registres. L'usage d'un accumulateur réduit le nombre d'accès mémoire par instruction, avoir plusieurs registres généraux le réduit encore plus. Au début de l'informatique, mémoire et processeur étaient tout aussi rapides, ce qui fait que les architectures à registre n'avaient pas d'avantage évident en termes de performances. Mais depuis, l’absence de registres est devenu un désavantage global.
| Classe d'architecture | Nombre d'accès mémoire par opération dyadique |
| Architecture à pile | Trois accès mémoire par opération : un par opérande, un pour le résultat |
| Architecture mémoire-mémoire | Trois accès mémoire par opération : un par opérande, un pour le résultat |
| Architecture à accumulateur | Un accès mémoire par instruction, pour lire la seconde opérande |
| Architecture à registres | Zéro si les opérandes sont dans les registres, un pour les opérations load-op, LOAD et STORE. |
L'absence de registre a cependant des avantages, voyons lesquels.

### La rapidité des interruptions/appels de fonction

L'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.

### La densité de code

Au début de l'informatique, la mémoire avait des performances similaires à celles du processeur, mais elle était de petite taille. Aussi il était avantageux d'avoir des programmes assez petits. Les architectures se distinguaient sur la densité de code, à savoir la taille des programmes. 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, ainsi que par le caractère CISC/RISC. La taille des instructions est aussi influencée par les modes d'adressage disponibles.

#### La taille des instructions : les architectures 0, 1, 2 et 3 références

La taille des instructions est avant tout une question d'encodage des instructions. Pour rappel, une instruction contient un opcode qui encode l'opération, un champ par opérande adressée explicitement, et éventuellement de quoi préciser le ou les modes d'adressage. L'opcode n'a aucune raison de changer entre les architectures canoniques. Par contre, le nombre d'opérandes adressées explicitement change grandement selon les architectures canoniques. 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.
Et cela nous amène à la distinction entre architectures 0, 1, 2 et 3 adresses. Elle est liée aux opérations dyadiques, à savoir celles à deux opérandes. 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. L'encodage des branchements est globalement le même d'une architecture à l'autre. Comme autre exemple, les instructions d'accès mémoire utilisent souvent l'adressage absolu, ce qui demande juste une adresse mémoire et un opcode, sur toutes les architectures canoniques. Les instructions d'accès mémoire PUSH et POP sont dans ce cas, pareil pour les instructions LOAD et STORE, pour les instructions pour copier une donnée dans l'accumulateur, etc.
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.
Les architectures à pile n'ont pas besoin de préciser la localisation des opérandes, qui sont au sommet de la pile. Elles sont adressées implicitement. Et cela vaut pour les instructions dyadiques comme monadiques. En conséquence, 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. Quelques architectures à pile ont des instructions annexes qui précisent l'adresse mémoire du second opérande, mais elles sont assez peu utilisées, la majorité des instructions adressent toutes les opérandes au sommet de la pile.
Sur les 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 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. Au vu de cet encodage, les architectures à accumulateur sont appelées architectures à une adresse par abus de langage.
Les architectures à deux et trois références regroupent les autres architectures à registre et mémoire-mémoire. Les architectures à deux références adressent les opérandes explicitement, mais le résultat est stocké à l'endroit indiqué par la référence du premier opérande. Pour le dire autrement, 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. Les architectures à trois références permettent de préciser le registre de destination du résultat explicitement, ou son adresse mémoire sur les architectures mémoire-mémoire. Ce genre d'architectures permet une meilleure utilisation des registres, mais les instructions deviennent plus longues que sur les architectures à deux références.
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, leurs 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

Les développements précédents nous parlent de la taille des instructions, mais il faut aussi tenir compte du nombre d'instructions. Et ce nombre est fortement influencé par l'architecture du processeur. Les 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 architectures canoniques. Et le nombre d'instructions 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 architectures canoniques.
Les machines à pile ont un nombre d'instructions par programme plus élevé que sur les autres architectures, en grande partie à 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 qui supportent des instructions load-op, 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'instructions augmente donc assez rapidement, mais reste en-deça de celui des architectures à pile. Les architectures LOAD-STORE ont un léger désavantage en plus, car les instructions load-op sont émulées avec une instruction LOAD suivie d'une instruction de calcul.
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 de sauvegarde/restauration des registres lors d'un appel de fonction | Instructions LOAD-STORE pour lire les opérandes et enregistrer les résultats. |
| Architecture à accumulateur | Instructions de sauvegarde/restauration des registres lors d'un appel de fonction | Rares copies de l'accumulateur en mémoire RAM. |
Les architectures CISC ont un avantage en termes de densité de code, par rapport aux architectures RISC. Le fait qu'elles intègrent des instructions complexes est un avantage. Les processeurs RISC doivent émuler ces instructions complexes à partir d'une suite d'opérations plus simples. Il n'est pas garanti que les instructions CISC soient plus rapides que leur équivalent RISC, tout dépend de leur implémentation matérielle. L'avantage est surtout que le programme prend moins d'instructions pour faire la même chose, ce qui augmente la densité de code.

#### La conclusion : la densité de code finale

Si 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.