Fonctionnement d'un ordinateur/Le chemin de données

Comme vu précédemment, le chemin de donnée est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité de communication avec la mémoire, et le ou les bus qui permettent à tout ce petit monde de communiquer. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres. La majeure partie des registres sont regroupés ensemble, mais quelques registres comme le registre d'état ou le program counter sont souvent séparés. Le ou les bus sont généralement assez complexes, il y en a souvent plusieurs.

Intérieur d'un processeur.

Les unités de calcul

modifier

Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée unité arithmétique et logique. Certains préfèrent l’appellation anglaise arithmetic and logic unit, ou ALU.

Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc. Mais laissons cela de côté pour le moment.

L'interface de l'ALU est assez simple : on a des entrées pour les opérandes, et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l'entrée de sélection de l'instruction, spécifie l'instruction à effectuer. Il faut bien prévenir notre unité de calcul qu'on veut faire une addition et pas une multiplication. Sur cette entrée, on envoie un numéro qui précise l'instruction à effectuer. La correspondance entre ce numéro et l'instruction à exécuter dépend de l'unité de calcul. Généralement, l'opcode de l'instruction est envoyé sur cette entrée, du moins sur les processeurs où l'encodage des instructions est "simple".

 
Unité de calcul usuelle.

La taille des opérandes de l'ALU

modifier

L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile. Mais il y a quelques exceptions, soit que l'ALU soit composées d'ALU plus petites, soit que l'ALU utilise des opérandes plus courtes que celles supportées par le processeur.

Le bit-slicing

modifier

Avant l'époque des premiers microprocesseurs 8 et 16 bits, le processeur n'était pas un circuit intégré unique, mais était formé de plusieurs puces électroniques soudées à la même carte. L'ALU était souvent une puce séparée, le séquenceur aussi, les registres étaient dans leur propre puce, etc. Les puces en question étaient des puces TTL assez simples, comparé à ce qu'on a aujourd'hui. Les ALU étaient vendues séparément, et elles manipulaient souvent des opérandes de 4/8 bits, les ALU 4 bit étant très fréquentes.

Si on voulait créer une ALU pour des opérandes plus grandes, il n'y avait pas le choix : il fallait construire l'ALU à partir de plusieurs ALU 4/8 bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul plus grosses à partir d’unités de calcul plus élémentaires s'appelle en jargon technique du bit slicing.

Cette technique est utilisée pour des ALU capables de gérer les opérations bit à bit, l'addition, la soustraction, mais guère plus. Il n'y a pas, à ma connaissance, d'ALU en bit-slicing capable d'effectuer une multiplication ou une division. La raison est qu'il n'est pas facile d'implémenter une multiplication entre deux nombres de 16 bits avec deux multiplieurs de 4 bits (idem pour la division). Alors que c'est plus simple pour l'addition et la soustraction : il suffit de transmettre la retenue d'une ALU à la suivante. Bien sûr, les performances seront alors nettement moindres qu'avec des additionneurs modernes, à anticipation de retenue, mais ce n'était pas un problème pour l'époque.

Les ALU aux opérandes courtes

modifier

Il arrive rarement que l'ALU manipule des opérandes plus petits que la taille des registres. Un exemple serait une ALU de 8 bits alors que les registres font 16 bits, ou une ALU 4 bits avec des registres de 8 bits. Autant cette solution est faisable sans trop de soucis avec l'addition ou la soustraction, autant la multiplication et la division s'implémentent difficilement quand registres et ALU n'ont pas la même taille.

Par exemple, sur le Z80, les registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. En conséquence, les calculs devaient être faits en deux phases : une qui traite les 4 bits de poids faible, et une autre qui traite les 4 bits de poids fort. L'unité de contrôle gérait tout cela, avec l'aide de registres placés en entrée/sortie de l'ALU, et de multiplexeurs/demultiplexeur. L'ensemble du circuit de l'ALU donnait ceci :

 
ALU du Z80

Un exemple extrême est celui des des processeurs sériels (sous-entendu bit-sériels), qui utilisent une ALU sérielle, qui fait leurs calculs bit par bit, un bit à la fois. N'allez pas croire que les processeurs sériels sont tous des processeurs de 1 bit. Certes, de tels processeurs ont existé, le plus connu d'entre eux étant le Motorola MC14500B. Mais beaucoup de processeurs 4, 8 et 16 bits étaient des processeurs sériels. Ils ont généralement un jeu d'instruction assez limité : des opérations logiques, l'addition, la soustraction, guère plus. Les ALU sérielles ne peuvent pas faire de multiplications ou de divisions.

Naturellement, effectuer les opérations bit par bit est plus lent comparé aux processeurs non-sériels. L'avantage de ces ALU est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes. Autant utiliser des registres longs est facile, autant une ALU non-sérielle avec des opérandes aussi grands aurait été impraticable à l'époque.

Un autre avantage est que ces ALU utilisent peu de circuits. Et c'est pour cette raison que beaucoup de processeurs parallèles utilisaient des ALU sérielles. Le but de ces processeurs était d'exécuter pleins de calculs en parallèles, d'exécuter plusieurs calculs simultanément. Et cela demande d'utiliser pleins d'unités de calcul distinctes : exécuter N opérations en parallèle demande N unités de calcul. Mais un grand nombre d'unités de calcul signifie que celles-ci doivent être très simples, les ALU sérielles étant tout indiquées avec cette contrainte.

Les ALU octet-sérielles sont des ALU de 8 bits, dans un processeur qui est lui 16, 32 ou 64 bits. Les calculs sont donc faits par paquets de 8 bits, non d'un seul coup. L'avantage est que le cout en matériel est assez faible, une ALU 8 bits n'utilise pas beaucoup de circuits. L'économie d'énergie est assez importante, vu que l'ALU est petite et utilise peu de circuits. Par contre, les performances sont généralement assez faible pour les calculs sur 16, 32 ou 64 bits. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles.

Il est possible de faire des multiplications ou divisions assez rapides avec des ALU octet-sérielles. Mais cela n'est pratique que sur les processeurs 16 bits. Par exemple, imaginez que vous vouliez multiplier deux opérandes entières de 16 bits, avec une ALU 8 bits. Il faudra faire quatre passages dans l'ALU : multiplier les octets de poids faibles des deux opérandes, faire pareil avec les octets de poids fort, multiplier l'octet de poids faible de la première opérande par l'octet de poids fort de la seconde, et enfin multiplier l'octet de poids faible de la seconde opérande par l'octet de poids fort de la première. Pour des opérandes de 32 bits, il faut cette fois-ci faire 16 passages !

Les opérations ont un temps de calcul variable. Les opérations sur des opérandes de 8 bits se font en un cycle d'horloge, mais celles sur 16 bits se font en deux cycles, celles en 32 en quatre, etc. Si un programme manipule assez peu d'opérandes 16/32/64 bits, la perte de performance est assez faible. Un autre point est que vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, ce qui fait que le temps de calcul sur 8 bits est plus faible, et la perte de performance pour les opérations 16/32/64 bits un peu atténuée. Et en parlant d'ALU allant à haute fréquence, parlons de l'ALU du Pentium 4, qui utilise une ALU de ce style.

Un cas particulier : l'unité de calcul du Pentium 4

modifier

Le Pentium 4 était un peu particulier dans son genre. Lui aussi avait une ALU à mi-chemin entre une ALU normale, et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont une était une ALU simple. Elle ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Mais elle ne gérait ni les multiplications, ni les décalages, qui étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe.

L'ALU simple était composée de deux sous-ALU de 16 bits chacune, bit-slicées. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé addition étagée (staggered add) dans la documentation Intel.

Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité est évidente quand on sait que l'ALU était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles.

La technique en question porte le nom barbare d'ALU double pumped, dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autre que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD.

Les unités de calcul spécialisées

modifier

Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Et certaines d'entre elles sont spécialisées dans des opérations spécifiques, qui ne sont techniquement pas des opérations entières, sur des nombres entiers.

Presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la floating-point unit, aussi appelée FPU. Néanmoins, ce regroupement des circuits pour nombres flottants n'est pas aussi strict qu'on pourrait le croire. Dans certains cas, les circuits capables d'effectuer les divisions flottantes sont séparés des autres circuits (c'est le cas dans la majorité des PC modernes) : tout dépend de l'architecture interne du processeur utilisé. Autrefois, ces FPU n'étaient pas incorporés dans le processeur, mais étaient regroupés dans un processeur séparé du processeur principal de la machine, appelé le coprocesseur arithmétique. Un emplacement dans la carte mère était réservé au coprocesseur. Ils étaient très chers et relativement peu utilisés, ce qui fait que seules certaines applications assez rares étaient capables d'en tirer profit : des logiciels de conception assistée par ordinateur, par exemple.

Il existe des unités de calcul spécialisées pour les calculs d'adresse. Elles gèrent moins d'opérations que les ALU normales, vu que peu d'opérations sont utiles pour les adresses. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciennes architectures.

Les anciens processeurs avaient un circuit incrémenteur séparé de l'unité de calcul. C'est le cas sur l'Intel 8085, le Z-80, et bien d'autres processeurs 8 bits. Il était utilisé pour incrémenter des adresses, ce qui est une opération très fréquente. Elle est utilisée pour manipuler des tableaux, le pointeur de pile, voire le program counter. Mais beaucoup d'architectures augmentaient ses capacités en lui permettant d'incrémenter des données. Pourtant, ce circuit incrémentait des nombres plus grands que l'ALU. Par exemple, c'est le cas sur le Z-80, où l'incrémenteur peut manipuler des nombres de 16 bits, alors que l'ALU ne peut gérer que des nombres de 8 bits.

De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, des instructions de test et des branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. les registres à prédicats sont situés juste en sortie de cette unité de calcul.

Les circuits multiplieurs et diviseurs

modifier

Les ALU des processeurs modernes sont souvent couplées à un circuit multiplieur séparé, avec éventuellement un circuit diviseur lui aussi séparé des autres. Précisons qu'il ne sert pas à grand chose de fusionner le circuit multiplieur avec l'ALU, mieux vaut les garder séparés par simplicité. Les processeurs à haute performance ont donc une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications.

La gestion des divisions est elle plus complexe. Il arrive que l'ALU pour les multiplication gère à la fois la multiplication et la division. Les circuits multiplieurs et diviseurs sont en effet très similaires et partagent beaucoup de points communs. Généralement, la fusion se fait pour les multiplieurs/diviseurs itératifs. Mais dans d'autres cas, le circuit diviseur est séparé des autres, ce qui fait qu'on a une ALU simple, une ALU pour les multiplications, et une ALU pour les divisions.

La présence d'un circuit multiplieur est très variable suivant le processeur, mais les circuits diviseurs sont eux très rares. Au vu de la complexité de ces circuits de multiplication/division, leur cout en circuit est à opposer au gain en performance lié au support de la multiplication. Le gain en performance pour la multiplication est modéré, alors qu'il est très faible pour la division, les deux circuits ont un cout en circuits similaire. Les processeurs haute performance disposent généralement d'un circuit multiplieur, et gèrent la multiplication dans leur jeu d'instruction. Le cas de la division est plus compliqué, et la plupart des CPU qui ne sont pas x86 n'en ont pas.

Il existe cependant des circuits qui se passent de multiplieurs, tout en supportant la multiplication dans leur jeu d'instruction. Certains utilisent pour cela du microcode, technique qu'on verra dans deux chapitres, mais l'Intel Atom utilise une technique franchement peu ordinaire. L'Intel Atom utilise la FPU pour faire les multiplications entières. Les opérandes entières sont traduites en nombres flottants, multipliés par la FPU, puis le résultat est converti en un entier avec quelques corrections à la clé. Ainsi, on fait des économies de circuits, en mutualisant le multiplieur entre FPU et ALU entière, surtout que ce multiplieur manipule des opérandes plus courtes. Les performances sont cependant réduites comparé à l'usage d'un vrai multiplieur entier.

Le barrel shifter

modifier

On vient d'expliquer que la présence de plusieurs ALU spécialisée est très utile pour implémenter des opérations compliquées à insérer dans une unité de calcul normale, comme la multiplication et la division. Mais les décalages sont aussi dans ce cas, de même que les rotations. Nous avions vu il y a quelques chapitres qu'ils sont réalisés par un circuit spécialisé, appelé un barrel shifter, qu'il est difficile de fusionner avec une ALU normale. Aussi, beaucoup de processeurs incorporent un barrel shifter séparé de l'ALU.

Les processeurs ARM utilise un barrel shifter, mais d'une manière un peu spéciale. On a vu il y a quelques chapitres que si on fait une opération logique, une addition, une soustraction ou une comparaison, la seconde opérande peut être décalée automatiquement. L'instruction incorpore le type de de décalage à faire et par combien de rangs il faut décaler directement à côté de l'opcode. Cela simplifie grandement les calculs d'adresse, qui se font en une seule instruction, contre deux ou trois sur d'autres architectures. Et pour cela, l'ALU proprement dite est précédée par un barrel shifter,une seconde ALU spécialisée dans les décalages. Notons que les instructions MOV font aussi partie des instructions où la seconde opérande (le registre source) peut être décalé : cela signifie que les MOV passent par l'ALU, qui effectue alors un NOP, une opération logique OUI.

Les registres du processeur

modifier

Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. Et là, les choses deviennent bien plus complexes. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres. Un banc de registres (register file) est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire.

 
Banc de registres simplifié.

Un processeur contient presque toujours un banc de registre, couplé à des registres isolés. Il y a cependant quelques exceptions, comme certaines architectures à accumulateur sans registres généraux, qui se passent de banc de registres. Mais dans une architecture normale, on trouve un ou plusieurs bancs de registres, et un ou plusieurs registres isolés. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.

L'adressage du banc de registres

modifier

Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d'identifiant de registre. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.

Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.

Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.

 
Adressage du banc de registres généraux

Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile peuvent être placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le program counter peuvent se mettre dans le banc de registre ! Nous verrons le cas du program counter dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.

 
Adressage du banc de registre - cas général

Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.

Les registres généraux

modifier

Pour rappel, les registres généraux sont des registres qui ne sont pas spécialisés et peuvent mémoriser n'importe quoi : des entiers, des flottants, des adresses, etc. Ils sont opposés aux registres spécialisés, où chaque registre est spécialisé dans un type de données.

Dans l'exemple pris dans cette section, le processeur n'a que des registres généraux, couplés à un program counter et un registre d'état. Le program counter et le registre d'état sont des registres isolés, les registres généraux sont rassemblés dans un banc de registre. Le banc de registre est appelé le banc de registres généraux, vu qu'il ne contient que ça et qu'ils sont tous dedans.

L'interface du banc de registres généraux

modifier

Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).

 
Banc de registre multiports.

L'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.

Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :

 
Register File d'une architecture à 2-adresses

Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :

 
Register File d'une architecture à 3-adresses

Les bancs de registres scindés

modifier

Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés.

Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.

Les architectures à registres spécialisés

modifier

Passons maintenant aux architectures dont les registres sont spécialisés. L'utilisation de plusieurs bancs de registres est plus simple et intuitive. Mais ce n'est pas systèmatiquement le cas et il est possible de regrouper des registres de type différents dans un seul banc de registres. Les deux méthodes, que nous allons détailler ci-dessous, portent respectivement les noms de banc de registre dédié et de banc de registre unifié.

L'exemple type est celui où on a des registres qui ne peuvent contenir qu'un type bien défini de donnée, par exemple des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs.

Mais d'autres processeurs utilisent un seul banc de registres unifié, qui regroupe tous les registres de données, qu'ils soient entier ou flottants. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.

 
Désambiguïsation de registres sur un banc de registres unifié.

Un autre exemple est celui des vieux processeurs où les adresses sont séparées des nombres entiers, dans deux ensembles de registres distincts. Les adresses et les données n'ont pas la même taille, ce qui fait que la meilleure solution est d'utiliser deux bancs de registres, un pour les adresses, l'autre pour les entiers. Le processeur Z80 faisait cela, en partie parce qu'il gérait des adresses de 16 bits, mais des données de 8 bits.

Le registre d'état

modifier

Le registre d'état fait souvent bande à part et n'est pas placé dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/flags provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/ou au bus interne au processeur.

Sa sortie est reliée au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non, et donc s'il faut faire ou non le branchement. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.

 
Place du registre d'état dans le chemin de données

Il est techniquement possible de mettre le registre d'état dans le banc de registre, mais cela complexifie l’implémentation du processeur. La principale difficulté est que les instructions arithmétiques doivent faire deux écritures dans le banc de registre : une pour le registre de destination, et une autre pour le registre d'état. Les deux écritures simultanées demandent d'utiliser un banc de registre à deux ports d'écriture, ce qui est très gourmand en transistors. Si le second port n'a pas l'occasion de servir pour d'autres instructions, c'est du gâchis. On pourrait aussi penser faire les deux écritures l'une après l'autre, mais cela demanderait de rajouter un registre en plus, ce qu'on cherche à éviter. Dans les faits, je ne connais aucun processeur qui utilise cette technique.

Les registres à prédicats

modifier

Les registres à prédicats sont, pour rappel, des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux. Ils sont généralement placés à part, dans un banc de registres séparé. Ils sont placés au même endroit que le registre d'état, et sont connectés de la même manière. Le banc de registres à prédicat a une entrée de 1 bit connectée à l'ALU, et une sortie de un bit connectée au séquenceur. L'unité de calcul écrit dans ce banc de registres à volonté, le séquenceur lit ces registres si besoin.

Et non seulement ils ont leur propre banc de registres, mais celui-ci est relié à une unité de calcul spécialisée dans les conditions/instructions de test. Parfois, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux, par exemple. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats, ce qui n'est possible qu'avec cette voie en lecture.

 
Banc de registre pour les registres à prédicats

Le pointeur de pile

modifier

Il n'est pas rare que certains processeurs aient des registres spécialisés pour le pointeur de pile et le frame pointer. Il est possible de mettre ces registres à part, en dehors du banc de registres, ou de les mettre dans le banc de registre. Sur les architectures où adresses et entiers sont dans des bancs de registre différents, le pointeur de pile est placé avec les adresses. Il faut dire que ces registres contiennent une adresse, pas un entier, et qu'il vaut mieux éviter de mélanger les deux.

Certains processeurs placent le pointeur de pile dans le banc de registre. C'est le cas sur le Z-80 ou sur l'Intel 8085, par exemple, où le pointeur de pile est dans le même banc de registre que les registres entiers (qui contient aussi les adresses). Le désavantage est qu'on perd un registre adressable : Avec un banc de registres de 16 registres, on ne peut plus en adresser que 15, le dernier étant pour un pointeur de pile non-adressable (en théorie). Mais l'avantage est que l'implémentation du processeur est plus simple. Les opérations réalisées sur le pointeur de pile sont de simples additions et soustractions, réalisées par l'ALU. Or, le banc de registre est déjà connecté à l'ALU, ce qui facilite l'implémentation des instructions de gestion de la pile, comme PUSH et POP.

 
Intel 8085.

L'autre solution est d'utiliser un registre isolé pour le pointeur de pile. L'avantage est que le pointeur de pile est censé être adressé implicitement. Pour le dire autrement, si on a un banc de registre de 16 registres, on a bien 16 registres adressables, alors que la solution précédente donne 15 registres adressables et un pointeur de pile qui ne l'est pas. L'inconvénient est dans la mise à jour du pointeur de pile, qui demande de l'incrémenter ou de le décrémenter. Soit on trouve un moyen pour le relier à l'ALU, soit on lui dédie un incrémenteur/décrémenteur spécialisé. Les deux solutions ajoutent des circuits et complexifient le chemin de données et le séquenceur.

Sur les vielles architectures, la solution est d'utiliser un incrémenteur spécialisé partagé avec le program counter. Le pointeur de pile est alors regroupé avec le program counter, les deux sont incrémentés par le même incrémenter. Un exemple est celui de l'Intel 4004, qui place les pointeurs de pile et le program counter dans un banc de registres séparé du reste du processeur. LA raison de cet arrangement est une économie de circuit : pas besoin d'utiliser deux incrémenteurs, on n'en utilise qu'un seul.

 
Microarchitecture de l'Intel 4040, une version améliorée de l'Intel 4004.

Le registre accumulateur

modifier

Pour rappel, les architectures à accumulateurs disposent d'un registre accumulateur, où on lit une opérande et enregistre le résultat. Les autres opérandes sont soit lues depuis la mémoire, soit lues depuis des registres nommés.

Le registre accumulateur, présent sur les architectures à accumulateur, n'est jamais mis dans le banc de registres, et est toujours un registre isolé, à part. Il est relié à l'unité de calcul comme indiqué dans le schéma ci-dessous. Il est souvent associé à d'autres registres temporaires qui servent à mémoriser les opérandes temporairement, le temps du calcul.

 
Accumulateur.

Si le processeur dispose de registres nommés, ils sont placés dans un banc de registres à part. Le banc de registre n'a qu'un seul port, qui sert à la fois pour la lecture et l'écriture. La raison est qu'on ne lit qu'une seule opérande depuis les registres, l'autre opérande étant lue depuis l'accumulateur. De plus, le résultat est enregistré dans l'accumulateur.

Les registres dédiés aux interruptions

modifier

Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Dans le cas qui va nous intéresser, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Les processeurs de ce type ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.

Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.

Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.

Le processeur Z80 utilisait cette technique. Avec cependant une petite différence : il avait un accumulateur séparé du banc de registre. Les six registres B, C, D, E, H et L étaient dans le banc de registre, de même que leurs copies pour interruptions nommées B', C', D', E', H' et L'. Par contre, l'accumulateur A et le registre d'état étaient aussi dupliqués : un pour les interruptions, l'autre pour les programmes. Le choix entre les deux se faisait par une bascule séparée pour les registres A,F,A',F', appelée la bascule A. Bascule qui parait redondante avec celle pour le banc de registres, mais qui ne l'est pas quand on sait ce qui va suivre.

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. Mais cela demandait de séparer l'accumulateur du reste.

Les instructions d'échange de registres du Z80 ne faisaient que modifier les bascules I et A. Cela évitait de faire des copies d'un ensemble de registre à l'autre. 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, ce qui est là encore fait par deux bascules : une par ensemble de registres.

Le fenêtrage de registres

modifier
 
Fenêtre de registres.

Le fenêtrage de registres fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.

Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.

Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.

 
Fenêtrage de registres au niveau du banc de registres.

L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.

 
Désambiguïsation des fenêtres de registres.

L'interface de communication avec la mémoire

modifier

L'interface avec la mémoire est, comme son nom l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la load-store unit, et j'en oublie.

 
Unité de communication avec la mémoire, de type simple port.

Sur certains processeurs, elle gère les mémoires multiport.

 
Unité de communication avec la mémoire, de type multiport.

Les registres d'interfaçage mémoire

modifier

L'interface mémoire se résume le plus souvent à des registres d’interfaçage mémoire, intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.

 
Registres d’interfaçage mémoire.

Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.

L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc. Cet avantage simplifie l'implémentation de certains modes d'adressage, comme on le verra à la fin du chapitre.

Les registres d’interfaçage sont aussi nécessaire pour implémenter certaines instructions, notamment tout ce qui implique de copier une donnée d'une adresse mémoire vers une autre. Un exemple de copie de ce genre est l'instruction COPY qui fait une copie d'une adresse mémoire vers une autre. Un autre exemple serait les instructions POP et PUSH des architectures à pile, qui font techniquement des copies en mémoire, d'une adresse mémoire vers le sommet de la pile en RAM. Une copie en mémoire se fait en deux étapes : on lit la donnée lors d'une première étape, on l'enregistre dans la seconde étape. La donnée lue est placée dans le registre d’interfaçage des données lors de la première étape, puis envoyée sur le bus de donnée lors de la seconde étape. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.

L'unité de calcul d'adresse

modifier

Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l'Address generation unit, ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.

Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.

 
Unité d'accès mémoire avec unité de calcul dédiée

Le fait d'avoir une unité de calcul séparée pour les adresses peut s'expliquer pour plusieurs raisons. Disons qu'il y a une différence à ce sujet entre les 5 architectures canoniques. La première raison est que cela simplifie un peu l'implémentation des modes d'adressage indirects sur les architectures LOAD-STORE, et à registre. Mais pour être franc, sur les architectures modernes, c'est avant tout une question de performance.

Par contre, sur les architectures anciennes, la raison principale était que les adresses et les entiers n'avaient pas la même taille. Il était courant pour des processeurs 8 bits d'avoir des adresses de 16 bits, par exemple. Dans ce cas, au lieu d'utiliser une ALU complexe de 16 bits, on utilisait une ALU de 16 bits très simple pour les adresses, et une ALU complexe de 8 bits pour les données. L'économie en circuit était assez importante. De plus, cela se mariait très bien avec le fait que les registres pour les adresses étaient séparés des registres entiers, ce qui nous amène à la section suivante.

Les registres d'adresse

modifier

Il y a quelques chapitres, nous avons vu que certains processeurs ont des registres séparés pour les entiers et les adresses. Typiquement, le processeur incorpore un banc de registre séparé pour les adresses. D'anciens processeurs utilisaient des registres d'indice, utilisés pour manipuler des tableaux, séparés des registres entiers. Les indices sont plus petits que les entiers normaux, ce qui fait qu'il vaut mieux utiliser un banc de registre séparé. Dans les deux cas, ces registres sont placés dans l'interface mémoire, juste avant l'unité de calcul d'adresse, seule à manipuler leur contenu.

 
Unité d'accès mémoire avec registres d'adresse ou d'indice

Sur certains processeurs, il arrive que le program counter soit placé dans le banc de registre pour les adresses et soit mis à jour par l'AGU. L'avantage est une économie de circuit : pas besoin de rajouter un troisième additionneur/incrémenteur. Après tout, le program counter est une adresse, et sa mise à jour est un calcul d'adresse comme un autre.

La gestion de l'alignement et du boutisme

modifier

L'interface mémoire est aussi celle qui gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés, et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gére automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.

Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, et les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés. Une vulgaire porte OU fait l'affaire, que ce soit dans l'exemple ou dans le cas général. Cette porte génère un signal qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.

La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.

Le rafraichissement mémoire optimisé et le contrôleur mémoire intégré

modifier

Depuis les années 80, les processeurs sont souvent combinés avec une mémoire principale de type DRAM. De telles mémoires doivent être rafraichies régulièrement pour ne pas perdre de données. Le rafraichissement se fait généralement adresse par adresse, ou ligne par ligne (les lignes sont des super-bytes internes à la DRAM). Le rafraichissement est en théorie géré par le contrôleur mémoire installé sur la carte mère. Mais au tout début de l'informatique, du temps des processeurs 8 bits, le rafraichissement mémoire était géré directement par le processeur.

Si quelques processeurs géraient le rafraichissement mémoire avec des interruptions, d'autres processeurs disposaient d’optimisations pour optimiser le rafraichissement mémoire. Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le processeur Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un timer interne permettait de savoir quand rafraichir la mémoire : quand ce timer atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le timer était reset. Et tout cela était intégré à l'unité d'accès mémoire.

Depuis les années 2000, les processeurs modernes ont un contrôleur mémoire DRAM intégré directement dans le processeur. Ce qui fait qu'ils gèrent non seulement le rafraichissement, mais aussi d'autres fonctions bien pus complexes.

Le bus interne au processeur

modifier

Pour échanger des informations entre les composants du chemin de données, on utilise un ou plusieurs bus internes. Toute micro-instruction configure ce bus, configuration qui est commandée par le séquenceur. Chaque composant du chemin de données est relié au bus via des transistors, qui servent d'interrupteurs. Pour connecter le banc de registres ou l'unité de calcul à ce bus, il suffit de fermer les bons interrupteurs et d'ouvrir les autres.

Les micro-architecture à un seul bus, à accumulateur interne

modifier

Dans le cas le plus simple, le processeur utilise un seul bus interne. Et ce bus unique ne permet de transmettre qu'une seule opérande à la fois, ce qui pose problème pour les opérations dyadiques, à deux opérandes. Avec cette organisation, la moindre opération demande d'exécuter plusieurs micro-instructions/micro-opérations. Une organisation de ce type a un lien avec les 5 architectures canoniques. Elle est utilisée sur les architectures à accumulateur, les architectures à pile et les architectures mémoire-mémoire.

Les architectures à pile et mémoire-mémoire

modifier
 
Chemin de données à un seul bus, principe général.

Voyons d'abord le cas des architectures à pile et mémoire-mémoire, où les opérandes des calculs sont lues depuis la mémoire et où le résultat est enregistré en mémoire. Le bus interne au processeur est alors connecté à la mémoire, éventuellement au pointeur de pile et au program counter (pour la gestion de la pile et les branchements). Considérez que tout ce qui est sur ce bus soit vient de la mémoire RAM, soit y est envoyé. Vous pouvez faire l'amalgame entre ce bus et le bus mémoire, et précisément avec le bus de données.

Pour gérer les instructions dyadiques, l'ALU est précédée d'un registre temporaire pour stocker une opérande, pendant que la seconde est fournie par le bus interne. De même, il est préférable d'utiliser un registre temporaire pour le résultat, pour les mêmes raisons. L’exécution d'une instruction dyadique prend plusieurs étapes, plusieurs micro-opérations : une pour copier la première opérande dans le registre, temporaire, une seconde pour lire la seconde opérande et faire le calcul, une troisième pour recopier le résultat du registre temporaire dans le banc de registres.

Il ne reste plus qu'à connecter une unité de communication avec la mémoire et un séquenceur pour obtenir un processeur fonctionnel. Il faut noter que les architectures mémoire-mémoire et à pile incorporent souvent des registres d'interfaçage pour les données et l'adresse, en plus des registres temporaires vus plus haut. Les architectures à pile en ont besoin pour gérer les instructions POP et PUSH, qui font techniquement des copies en mémoire, d'une adresse vers le sommet de la pile en RAM. Une copie en mémoire lit la donnée lors d'une première étape, avant de l'enregistrer dans la seconde étape, en passant par le registre d'interfaçage mémoire entre les deux.

Les architectures à accumulateur

modifier

Passons ensuite aux architectures à accumulateur simples, sans registres d'indices. Dans ce cas, les deux registres temporaires vus plus haut sont fusionnés en un seul registre, qui n'est autre que l'accumulateur. Elles se résument alors à une ALU, l'accumulateur et l'unité de contrôle qu'on détaillera dans le chapitre suivant. Il est possible d'ajouter des registres d’interfaçage, mais pas forcément.

 
Architecture à accumulateur de type Harvard
 
Architecture à accumulateur basique

Essayons de comprendre comment est conçue une architecture à accumulateur en détail. Nous allons prendre une architecture Harvard pour simplifier les choses. Les schémas qui vont suivre remplacent le bus de données par la mémoire de données. Et celle-ci est multiport, pour éviter les détails d'implémentation liés aux registres d’interfaçage.

La première brique est composée d'une ALU reliée à l'accumulateur et au bus de données. Il faut aussi gérer le cas des opérations LOAD/STORE, au minimum l'opération STORE. L'opération STORE demande simplement de connecter l'accumulateur au bus de données, sur le port d'écriture, comme illustré ci-dessous. Une solution alternative est de connecter la sortie de l'ALU au bus de données, sur le port d'écriture.

 
Machine à accumulateur.
 
Machine à accumulateur.

Si on ajoute un program counter et une unité de décodage, on obtient le processeur final. Pour une architecture Harvard, on obtient ceci, à ceci près que l'unité de décodage n'est pas représentée. Imaginez qu'elle est en sortie de la mémoire d'instruction. Vous remarquerez que l'accumulateur n'est pas connecté directement au bus mémoire, mais que c'est la sortie de l'ALU qui l'est. La raison est que l'ALU peut effectuer une opération NOP qui recopie l'accumulateur en sortie. Simple détail d'implémentation, qui rend l'implémentation du processeur légèrement plus simple dans certains cas.

 
Machine à accumulateur.

En ajoutant de quoi gérer les branchements, on obtient ceci. Malheureusement, nous verrons à quoi servent ces circuits de gestion des branchements dans le chapitre suivant. Pour simplifier, on ajoute un registre d'état, relié à une unité de gestion des branchements, qui altére le program counter.

 
Machine à accumulateur.

Les architectures à accumulateur plus évoluées ajoutent des interconnexions plus complexes, pour gérer certains modes d'adressages un peu spécialisés. Vous remarquerez que les architectures à accumulateur ont une implémentation similaire aux architectures mémoire-mémoire/à pile. L'accumulateur prend la place des registres temporaires de l'ALU, le cout en circuit est relativement similaire pour le chemin de données. La différence est que l'accumulateur est un registre architectural, adressé implicitement, là où les autres architectures cachent ces registres au programmeur. Les architectures à accumulateur exposent des registres autrefois cachés, et permettent au programmeur de contrôler ce registre.

Un petit aparté sur les registres d'indice. Nous n'en avons pas parlé jusqu’à présent, car ils n'ont rien à voir avec l'ALU. La majorité des architectures à accumulateur disposait d'une ALU spécialisée pour les calculs d'adresse, intégrée dans l'unité de communication avec la mémoire. Et cette ALU a une entrée connectée sur le bus interne du processeur, et une autre aux registres d'indice. L'unité de communication avec la mémoire reçoit une adresse depuis le bus interne, lit en parallèle le registre d'indice sélectionné, puis fait le calcul d'adresse demandé dans l'ALU. La raison à cela est que les adresses et données n'ont souvent pas la même taille sur les architectures à accumulateur, sans compter que l'implémentation est plus simple.

Les autres architectures : LOAD-STORE et à registres

modifier

Une telle organisation à un seul bus est aussi possible sur des architectures à registre ou LOAD-STORE. Dans ce cas, le processeur incorpore un banc de registre, qui est connecté sur le bus interne unique. Le banc de registre est mono-port, on ne peut lire qu'une opérande à la fois dedans. Une première opérande lue est envoyée dans le registre temporaire avant l'ALU, la seconde opérande est envoyée à l'ALU directement. Le résultat est stocké dans le registre temporaire en sortie de l'ALU, puis est envoyé dans le banc de registre. La communication avec la mémoire est aussi simplifiée. Lors d'un accès mémoire, le banc de registre est connecté sur le bus interne, lui-même connecté au bus mémoire.

L'usage d'un banc de registre mono-port complique l'implémentation de certains modes d'adressage. Par exemple, c'est le cas de l'adressage indirect à registre. La raison est qu'il demande de faire deux accès au banc de registre : un pour récupérer l'adresse à envoyer sur le bus d'adresse, puis un autre pour gérer la donnée lue/à écrire. Pour une lecture, la donnée lue est enregistrée dans le registre de destination, et est donc envoyée en écriture au banc de registres. Pour une écriture, la donnée à écrire en mémoire est lue depuis le banc de registre.

Faisons les comptes pour voir combien d'accès au banc de registre demande une lecture/écriture en adressage indirect. L'écriture demande d'accéder à deux registres en même temps, alors que les deux accès sont plus distants pour une lecture (ils sont séparés par l'accès mémoire proprement dit). L'écriture demande deux accès en lecture, la lecture demande un accès en lecture et un accès en écriture. Les deux accès peuvent être faits en même temps si le banc de registre est multiport, avec précisément deux ports de lecture et un d'écriture, mais c'est impossible s'il n'a qu'un seul port.

Pour résoudre le problème, l'unité de communication avec la mémoire doit incorporer des registres d'interfaçage, et les connecter le banc de registre si besoin. Une lecture en adressage indirect copie l'adresse du banc de registre vers le registre d’interfaçage d'adresse, effectue la lecture, puis copie la donnée lue depuis le registre d’interfaçage de données vers le banc de registre. C'est possible car le banc de registre est libre après avoir envoyé l'adresse : le registre d’interfaçage maintient l'adresse, ce qui libère le port de lecture/écriture du banc de registre, le laissant libre pour l'enregistrement de la donnée lue. Pour les écritures en adressage indirect, il faut lire la donnée à écrire et l'adresse en deux fois, et les stocker dans les registres d’interfaçage adéquats.

Les micro-architectures à plusieurs bus

modifier

La plupart des processeurs à registres et LOAD-STORE s'arrangent pour relier les composants du chemin de données en utilisant plusieurs bus, histoire de simplifier la conception du processeur ou d'améliorer ses performances. Le cas le plus simple est celui des architectures de type LOAD-STORE, mais les architectures à registres ne sont pas fondamentalement différentes. Elles ont pour avantages que les opérations simples ne nécessitent qu'une ou deux micro-opérations/micro-instructions, pas plus. Alors que sur les architectures à un seul bus, une opération dyadique prenait trois micro-opérations, elles n'en prennent qu'une seule sur les architectures à plusieurs bus internes.

Une architecture LOAD-STORE basique

modifier

Pour commencer, nous allons étudier le cas le plus simple possible : celui d'une architecture LOAD-STORE avec un nombre minimal de modes d'adressages. L'architecture supporte les modes d'adressage sles plus simples, ceux pour les données, elle ne gère pas les adressages pour les pointeurs : pas d'adressage indirect, pas d'adressage indicé, etc. Pour commencer, l'architecture supporte deux modes d'adressages : inhérent (à registres), et absolu (par adresse mémoire). Nous rajouterons l'adressage immédiat après.

Les instructions arithmétiques et les branchements utilisent toutes les registres, mais ne vont pas chercher leurs opérandes en RAM. Elles s'exécutent donc en utilisant l'ALU et le banc de registres, rien de plus. L'usage de plusieurs bus internes permet d'utiliser un banc de registre multiport, ce qui facilite grandement l'implémentation des instructions dyadiques. Par exemple, on peut avoir deux ports de lecture, ce qui permet de lire deux opérandes à la fois, une par port de lecture. De même, on peut rajouter un port d'écriture sur le banc de registre, pour faciliter l'écriture du résultat dans le banc de registres. Les opérations arithmétiques lisent deux opérandes depuis deux ports de lecture, font le calcul dans l'unité de calcul, puis connectent la sortie de l'ALU sur l'entrée d'écriture du banc de registres.

L'instruction LOAD et STORE utilisent l'adressage absolu, c'est à dire que l'adresse à lire/écrire est intégrée à l'instruction elle-même. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Reste à gérer l'échange entre registres et bus de données. Effectuer une lecture LOAD demande de relier le bus de données sur l'entrée d'écriture du banc de registres. L'écriture demande de faire l'inverse : de connecter la sortie de lecture du banc de registre vers le bus mémoire.

 
Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)

Avec quelques multiplexeurs, on arrive à gérer les déplacements de données. Le schéma ci-dessous illustre le résultat. Nous n'avons pas représenté les connexions entre le séquenceur, sauf pour le bus d'adresse. Pour rappel, il doit envoyer les noms de registres adéquats au banc de registre, envoyer le code opération à l'unité de calcul, et surtout : configurer les multiplexeurs.

 
Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport

Ajoutons ensuite les instructions de copie entre registres. Elles existent sur la plupart des architectures LOAD-STORE, mais cela ne signifie pas que l'on doit modifier le chemin de données pour cela. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU ne fait rien et recopie simplement une de ses opérandes sur sa sortie. Une autre solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.

 
Chemin de données d'une architecture LOAD-STORE

L'ajout des modes d'adressage pour les pointeurs

modifier

Passons maintenant à l'implémentation des modes d'adressages pour les pointeurs. Avec eux, l'adresse mémoire à lire/écrire n'est pas intégrée dans une instruction, mais est soit dans un registre, soit calculée par l'ALU. Pour simplifier les schémas, nous allons omettre le cas où les copies entre registres passent par l'ALU, afin d'enlever une voie de transfert possible. Mais nous allons supposer que cette voie existe, et qu'elle est implémentée en ajoutant quelques multiplexeurs et démultiplexeurs.

Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre. Pour rappel, l'adressage indirect à registre correspond au cas où un registre contient l'adresse à lire/écrire. L'implémenter demande donc de connecter la sortie des registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un des port de lecture, ce qui donne ceci :

 
Chemin de données à trois bus.

Le mode d'adressage base + indice est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Le calcul de l'adresse implique au minimum une addition et implique donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.

 
Bus avec adressage base+index

Le chemin de données précédent ne gère pas que le mode d'adressage Base + Indice, mais aussi le mode d'adressage indirect avec pré-décrément. Pour rappel, ce mode d'adressage est une variante du mode d'adressage indirect, qui utilise une pointeur/adresse stocké dans un registre. La différence est que ce pointeur est décrémenté avant d'être envoyé sur le bus d'adresse. L'implémentation matérielle est la même que pour le mode Base + Indice : l'adresse est lue depuis les registres, décrémentée dans l'ALU, et envoyée sur le bus d'adresse en sortie de l'ALU.

Le mode adressage base+index et le mode d'adressage indirect à registre demandent de connecter le bus d'adresse sur le chemin de données, mais l'une doit le faire avant l'ALU et l'autre après. Mais si l'ALU supporte l'opération NOP, les deux peuvent passer par l'ALU, puis être redirigées vers le bus d'adresse. Pour gérer le mode d'adressage indirect, il suffit que l'ALU gère une instruction qui ne fait rien, un NOP. La seule différence est que l'ALU fera une opération NOP pour le mode d'adressage indirect à registre, et un calcul d'adresse pour le mode d'adressage base + index. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.

 
Bus avec adressages pour les pointeurs, simplifié.

Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent. Une bonne partie des processeurs RISC ne sont pas dans ce cas, soit parce qu'ils se contentent d'un adressage indirect à registre, soit parce qu'ils ont bien une voie directe entre bus d'adresse et registres. Par contre, choisir l'implémentation du schéma précédent permettra d'avoir des schéma plus lisibles dans ce qui suit.

L'adressage immédiat et les modes d'adressages exotiques

modifier

Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. L'implémenter demande d'extraire cette constante de l'instruction et de l'insérer au bon endroit dans le chemin de données. La constante est extraite et fournie par le séquenceur. Pour les opérations arithmétiques/logiques/branchements, il faut insérer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors subir une extension de signe dans un circuits spécialisé.

 
Chemin de données - Adressage immédiat avec extension de signe.

Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a pas de changement avec l'implémentation précédente, mais seulement si l'unité de calcul est capable d'effectuer une opération NOP, c'est à dire une opération qui ne fait rien et recopie la première opérande sur sa sortie. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouver dans les registres. Mais si l'ALU ne gère pas les NOP, la constante doit être envoyée sur l'entrée d'écriture du banc de registres. Et il faut rajouter un MUX pour cela.

 
Implémentation de l'adressage immédiat dans le chemin de données

L'implémentation précédente a un gros avantage : elle ajoute le support des adressages base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constant et lui ajoute un indice pris dans les registres. Dans les deux cas, on lit un registre, extrait une constante/adresse de l’instruction, additionne les deux dans l'ALU, avant d'envoyer le résultat sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses. Le mode d'adressage direct peut être traité de la même manière, si l'ALU est capable de faire des NOPs. L'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse.

 
Chemin de données avec adressage immédiat étendu pour gérer des adresses.

L'implémentation d'un processeur à registre

modifier

Tout ce qu'on a vu parle d'un processeur de type LOAD-STORE, où les lectures/écritures sont séparées du reste, ici des instructions utilisant l'ALU. Mais un processeur à registre gère souvent des instructions arithmétiques qui peuvent lire une opérande depuis la mémoire. Le cas le plus simple est celui où une opérande est lue en mémoire RAM/ROM, alors que l'autre est dans les registres. Et les jeux d'instruction peuvent autoriser ce genre de choses pour plusieurs opérandes, voire pour toutes. Il est par exemple possible de lire les deux opérandes en mémoire. Et pire, il y a certains processeurs qui autorisent d'écrire le résultat directement en mémoire, sans passer par les registres.

Les trois cas s'implémentent différemment. Mais l'idée est de relier le bus de donnée directement aux entrées/sorties de l'unité de calcul. Précisons que l'instruction s'exécute souvent en plusieurs étapes, en plusieurs micro-instructions, en plusieurs micro-opérations. Il y a typiquement une étape par opérande à lire en mémoire, puis une étape de calcul,et potentiellement une étape pour enregistrer le résultat en mémoire.

Dans ce qui va suivre, nous allons supposer que le processeur incorpore un ou plusieurs registres d’interfaçage sur le bus de données. Avec ce registre, une lecture se fait en deux étapes : la donnée est lue en RAM/ROM et copiée dans ce registre d’interfaçage, puis on recopie son contenu dans les registres lors d'une seconde étape. L'écriture est similaire : la donnée est d'abord écrite dans ce registre, puis envoyée sur le bus mémoire. Dans tous les schémas précédents, ce qui est noté "bus de données" peut être remplacé par un registre d’interfaçage sans problème, c'est un détail d'implémentation peu pertinente pour ce qui nous intéresse. Et il en sera le même pour les schémas qui vont suivre : remplacer le registre d’interfaçage par le bus de données est possible, cela change juste un peu l'implémentation.

L'usage d'un registre d’interfaçage permet d'implémenter certaines instructions très facilement. Typiquement, tout ce qui implique de copier une donnée d'une adresse mémoire vers une autre. Elle se fait en deux étapes : on lit la donnée lors d'une première étape, on l'enregistre dans la seconde étape. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes. Un exemple de copie de ce genre est l'instruction COPY qui fait une copie mémoire vers mémoire, ou une instruction MOV utilisée avec les bons modes d'adressage. Les processeurs x86 ne supportent pas une telle opération.

Le cas le plus simple est celui où une opérande seulement peut être lue depuis la mémoire. Dans ce cas, il suffit de relier le registre d’interfaçage sur une des entrées de l'ALU, un simple multiplexeur suffit. Pour une instruction LOAD/STORE, on connecte le registre d’interfaçage aux registre, avant ou après l'accès mémoire proprement dit. Et il est possible de se contenter d'un seul registre d’interfaçage, vu qu'on n'a pas de cas où une lecture se passe en même temps qu'une écriture.

 
Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire

Pour gérer le cas où deux opérandes sont en mémoire, il faut rajouter un second registre d’interfaçage en lecture, qui peut être relié sur l'autre entrée de l'ALU. Mais de tels cas sont rares, beaucoup de jeux d'instruction n'autorisent pas ce genre de fantaisies.

Le cas où le résultat peut être écrit directement en mémoire est géré en reliant la sortie de l'ALU sur le bus de données, ou le registre d’interfaçage pour les données. L'usage d'un registre d’interfaçage est absolument nécessaire pour le cas où une instruction lise une opérande depuis la mémoire et enregistre le résultat en mémoire. Sans registre d’interfaçage, on lit une opérande sur le bus en même temps qu'on cherche à écrire le résultat dessus. Et c'est en même temps car il n'y a pas de registre pour mémoriser l'opérande lue, tout se fait lors du même cycle. Mais avec un registre d’interfaçage, la lecture et l'écriture sont séparées. L'instruction a lieu comme suit : on copie l'opérande de la mémoire dans le registre d’interfaçage, on effectue l'opération au cycle suivant, on enregistre le résultat dans le registre d’interfaçage au cycle suivant, on envoie le résultat en mémoire au dernier cycle.

 
Chemin de données d'un CPU CISC avec écriture du résultat en mémoire

Annexe : le clock/power gating du chemin de données

modifier

Afin de réduire la consommation d'énergie du processeur, une partie du chemin de données peut être désactivé, mis en veille. Dans son implémentation la plus simple, les unités inutilisées ne reçoivent plus le signal d'horloge. Il s'agit de la technique du clock gating vues il y a de cela plus d'une dizaine de chapitres, dans le chapitre sur la consommation électrique des circuits. Il est aussi possible d'utiliser des techniques comme l'évaluation gardée ou le power gating (couper l'alimentation), mais c'est déjà plus rare. Et le clock gating peut s'implémenter à différents niveaux dans le chemin de données.

Un point important est que l'unité de contrôle n'est pas désactivée, alors que le chemin de données l'est si besoin. Concrètement, l'unité de contrôle/chargement doit charger une nouvelle instruction régulièrement : à chaque cycle, ou après quelques cycles. Et ces instructions doivent être décodées pour savoir si il faut les exécuter, comment configure"r le chemin de données, etc. Les possibilités d'éteindre l'unité de chargement et de contrôle sont limitées, pour ne pas dire inexistantes. Par contre, éteindre une partie du chemin de données est bien plus fréquent.

Le clock gating des registres et unités de calcul

modifier

La première méthode de clock gating consiste à désactiver les unités de calcul ou les registres inutilisés.

Par exemple, prenons une instruction de calcul dont les opérandes sont dans les registres, en adressage inhérent. Les unités de communication avec la mémoire sont inutilisées : on peut les désactiver à grand coup de clock gating. Même chose lors d'un accès mémoire : on calcule l'adresse dans l'ALU, puis celle-ci est inutilisée lors de l'accès en RAM proprement dit. On peut alors désactiver l'ALU une fois l'adresse calculée, ce qui permet de la désactiver durant quelques dizaines ou centaines de cycles. Idem pour les registres, inutilisés lors de l'accès mémoire proprement dit. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible.

Un autre exemple, bien plus intéressant, est celui des opérations flottantes ou entières, sur les processeurs avec une ALU entière et une FPU. Dans ce cas, il est possible de désactiver l'ALU entière pendant les instructions de calcul flottant, et inversement de désactiver la FPU pendant les instructions entières. Il est aussi possible de faire pareil avec les registres entiers/flottants s'ils sont inutilisés. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres pour entiers avec du clock gating permet de gagner en énergie assez simplement. De plus, il est très rare qu'un programme entrelace des instructions flottantes et entières. Ce qui fait que l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. Les gains sont substantiels. Le gain est encore supérieure avec la désactivation de la FPU et des registres flottants, qui ont gourmands en circuits et en énergie.

La désactivation des unités inutilisée est commandée par l'unité de contrôle. En effet, une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction, et quelles sont celles inutilisées. Elles sait donc quelles unités activer ou désactiver. Pour configurer le clock gating, l'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de clock gating, l'unité de contrôle doit être conçue pour. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le clock gating lui permettait d'économiser 25% d'énergie.

Le clock gating de l'ALU pour les opérandes courtes

modifier

Une autre source de clock gating est le fait que les opérandes sont généralement assez courtes, à savoir qu'elles font 8, 16 bits, rarement plus. En effet, beaucoup de calculs d'adresse utilisent des indices codés sur 16 bits, guère plus, et beaucoup de calculs entiers ne font pas mieux. Avec des opérandes courtes, sur un processeur 32 ou 64 bits, les bits de poids forts sont toujours à zéro. Il est alors possible de désactiver les entrées de l'ALU qui restent les mêmes d'une opération sur l'autre, avec clock gating ou évaluation gardée.

Une première solution est possible sur les processeurs avec des instructions entières différentes pour chaque taille d'opérande. Par exemple, certains processeurs ont des instructions différentes pour les opérandes 8 bits, 16 bits, 32 bits et 64 bits. Dans ce cas, le clock gating dépend de l'instruction utilisée, l'unité de contrôle sait quels registres d'entrée de l'ALU désactiver. Mais elle est peut utile car les instructions en question sont peu utilisées, le compilateur n'en profite généralement pas.

Une technique plus élaborée détecte les opérandes courtes lors de l’exécution, sans aide du jeu d'instruction. Elle classe les opérandes en deux types : celles qui font 16 bits ou moins, celles qui font plus. Avec les premières, les 48 bits de poids fort sont à 0, ce qui n'est pas le cas pour les secondes. L'ALU est précédée par plusieurs registres, qui mémorisent les opérandes. Il y a deux registres pour chaque opérande : un registre 16 bits poids les bits de poids fort de l'opérande, 48 pour les bits de poids fort. Le registre de poids fort peut être figé avec clock gating ou évaluation gardée si l'opérande est courte.

Reste à détecter les opérandes courtes et à les séparer du reste. Pour cela, on ajoute un circuit en sortie de l'ALU, qui vérifie si le résultat est court ou non. Rien de plus simple : il suffit de vérifier que les 48 bits de poids fort sont à 0 ou non. Le résultat se voit attribuer un bit qui indique s'il code une valeur courte ou non. Ce bit est ajouté au nombre, il le suit dans tout le processeur, il est même mémorisé dans le banc de registre. Si le résultat est utilisé plus tard comme opérande d'un calcul, le bit est utilisé par les circuits de clock gating associés à l'ALU.