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. Le ou les bus sont généralement assez complexes, il y en a souvent plusieurs. DAns ce chapitre, nous allons voir ces composants en détail.
Les unités de calcul
[modifier | modifier le wikicode]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. Par défaut, ce terme est réservé aux unités de calcul qui manipulent des nombres entiers. Il existe des unités de calcul spécialisées pour les calculs flottants, mais elles sont désignées par le terme "unité de calcul flottant", ou encore FPU (Floating Point Unit).
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".

L'ALU entière : additions, soustractions, opérations bit à bit
[modifier | modifier le wikicode]Un processeur contient plusieurs ALUs spécialisées. La principale, présente sur tous les processeurs, est l'ALU entière. Elle s'occupe des opérations sur des nombres entiers uniquement, les nombres flottants sont gérés par une ALU à part. Elle gère des opérations simples : additions, soustractions, opérations bit à bit, parfois des décalages/rotations. Par contre, elle ne gère pas la multiplication et la division, qui sont prises en charge par un circuit multiplieur/diviseur à part.
Une ALU entière gère souvent une opération particulière, qui ne fait rien et recopie simplement une de ses opérandes sur sa sortie. L'opération en question est appelée l'opération Pass through, encore appelée opération NOP. Elle est implémentée en utilisant un simple multiplexeur, placé en sortie de l'ALU. Le fait qu'une ALU puisse effectuer une opération Pass through permet de fortement simplifier le chemin de donnée, d'économiser des multiplexeurs. Mais nous verrons cela sous peu.

L'ALU entière a déjà été vue dans un chapitre antérieur, nommé "Les unités arithmétiques et logiques entières (simples)", qui expliquait comment en concevoir une, quels sont ses circuits. Nous avions vu qu'une ALU entière est une sorte de circuit additionneur-soustracteur amélioré, ce qui explique qu'elle gère des opérations entières simples, mais pas la multiplication ni la division. Nous ne reviendrons pas dessus. Cependant, il y a des choses à dire sur leur intégration au processeur.
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 | modifier le wikicode]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 manipulaient des opérandes de 4/8 bits, les ALU 4 bits étant très fréquentes.
Si on voulait créer une ALU pour des opérandes plus grandes, il fallait construire l'ALU en combinant 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 à partir d'unités de calcul plus élémentaires s'appelle en jargon technique du bit slicing.
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 | modifier le wikicode]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. Un exemple assez connu est celui du Motorola 68000, qui était un processeur 32 bits, mais dont l'ALU faisait juste 16 bits. Son succeseur, le 68020, avait lui une ALU de 32 bits.
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/démultiplexeur.

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. S'il a existé des processeurs de 1 bit, comme le Motorola MC14500B, la majeure partie des processeurs sériels étaient des processeurs 4, 8 ou 16 bits. L'avantage de ces ALU est qu'elles utilisent peu de transistors, au détriment des performances par rapport aux processeurs non-sériels. Mais un autre avantage est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes.
Les ALU octet-sérielles sont des ALU de 8 bits, dans un processeur qui est lui 16, 32 ou 64 bits. L'avantage est qu'une ALU 8 bits n'utilise pas beaucoup de transistors, mais cela se fait au détriment des performances. Les opérations sur des opérandes 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. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles. Par exemple, vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, pour réduire la perte de performance. 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 | modifier le wikicode]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 circuits multiplieurs et diviseurs
[modifier | modifier le wikicode]Les processeurs modernes ont une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications, un circuit multiplieur séparé. 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 disposent systématiquement d'un circuit multiplieur et gèrent la multiplication dans leur jeu d'instruction.
Le cas de la division est plus compliqué. La présence d'un circuit multiplieur est commune, mais les circuits diviseurs sont eux très rares. Leur cout en circuit est globalement le même que pour un circuit multiplieur, mais le gain en performance est plus faible. Le gain en performance pour la multiplication est modéré car il s'agit d'une opération très fréquente, alors qu'il est très faible pour la division car celle-ci est beaucoup moins fréquente.
Pour réduire le cout en circuits, 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.
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 l'unité de calcul flottante pour faire les multiplications entières. Les opérandes entières sont traduites en nombres flottants, multipliés par l'unité de calcul flottante, 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 l'unité de calcul flottante et l'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 | modifier le wikicode]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 unités de calcul spécialisées
[modifier | modifier le wikicode]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 registres du processeur
[modifier | modifier le wikicode]Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres comme le registre d'état ou le program counter. 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. Dans processeur moderne, on trouve un ou plusieurs bancs de registres. 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 | modifier le wikicode]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.

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.

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 | modifier le wikicode]Pour rappel, les registres généraux peuvent mémoriser des entiers, des adresses, ou toute autre donnée codée en binaire. Ils sont souvent séparés des registres flottants sur les architectures modernes. Les registres généraux sont rassemblés dans un banc de registre dédié, appelé le banc de registres généraux. 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).

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 :

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 :

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 registres flottants : banc de registre séparé ou unifié
[modifier | modifier le wikicode]Passons maintenant aux registres flottants. Intuitivement, on a 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.

Le registre d'état
[modifier | modifier le wikicode]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.
Le registre d'état est relié 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. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet de lire son contenu et de le copier dans un registre de données. Cela permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.

L'ALU fournit une sortie différente pour chaque bit du registre d'état, la connexion du registre d'état est directe, comme indiqué dans le schéma suivant. Vous remarquerez que le bit de retenue est à la fois connecté à la sortie de l'ALU, mais aussi sur son entrée. Ainsi, le bit de retenue calculé par une opération peut être utilisé pour la suivante. Sans cela, diverses instructions comme les opérations add with carry ne seraient pas possibles.

Il est techniquement possible de mettre le registre d'état dans le banc de registre, pour économiser un registre. La principale difficulté est que les instructions doivent faire deux écritures dans le banc de registre : une pour le registre de destination, une pour le registre d'état. Soit on utilise deux ports d'écriture, soit on fait les deux écritures l'une après l'autre. Dans les deux cas, le cout en performances et en transistors n'en vaut pas le cout. D'ailleurs, je ne connais aucun processeur qui utilise cette technique.
Les registres à prédicats
[modifier | modifier le wikicode]Les registres à prédicats remplacent le registre d'état sur certains processeurs. Pour rappel, les registres à prédicat sont 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 placés à part, dans un banc de registres séparé. Le banc de registres à prédicats a une entrée de 1 bit connectée à l'ALU et une sortie de un bit connectée au séquenceur. Le banc de registres à prédicats est parfois relié à une unité de calcul spécialisée dans les conditions/instructions de test. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats. Pour cela, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux.

Le pointeur de pile
[modifier | modifier le wikicode]Les anciens processeurs avaient un registre spécialisé pour le pointeur de pile. Il est possible de mettre ce registres à part, en dehors du banc de registre, ou au contraire dans le banc de registre. La solution choisie dépend beaucoup du jeu d'instruction du processeur. Pour rappel, seules quelques instructions spécifiques peuvent modifier ce registre.
Le premier cas à étudier est celui où le processeur ne gère pas une pile d'appel, mais une simple pile d'appel de retour. Nous omettons volontairement le cas où l'architecture utilise directement une mémoire LIFO intégrée au processeur pour implémenter la pile d'adresses de retour. En effet, dans ce cas, il n'y a pas de pointeur de pile proprement dit. Le cas intéressant est celui où il y a un pointeur de pile altéré uniquement par les instructions CALL et RET, d'appel et de retour de fonction. Il est alors incrémenté ou décrémenté de la taille d'une adresse, donc d'une valeur fixe. Il est alors intéressant d'utiliser un registre isolé pour le pointeur de pile, qui de plus a son propre circuit incrémenteur.
D'autres processeurs gèrent une pile d'appel simple, avec un support limité aux instructions PUSH et POP, éventuellement des instructions spécifiques pour additionner une constante au pointeur de pile. Le pointeur de pile n'est cependant pas adressable . Là encore, le pointeur de pile est incrémenté/décrémenté de la taille d'une opérande ou d'une adresse, les deux étant souvent identiques.
Il est là aussi intéressant d'utiliser un registre isolé pour le pointeur de pile, qui de plus a son propre circuit incrémenteur. Il est aussi possible de relier le pointeur de pile à l'unité de calcul, qui s'occupe alors de faire les incrémentations/décrémentations. La première solution est préférée si les adresses et opérandes sont de taille différentes. Par exemple, si le processeur gère des données de 8 bits, mais des adresses de 16 bits, l'ALU fera 8 bits et sera trop courte pour incrémenter/décrémenter le pointeur de pile. Un incrémenteur séparé règle le problème.
Enfin, il y a des processeurs qui ont un pointeur de pile adressable. Dans ce cas, il est préférable de placer 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). 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.

Les registres dédiés aux interruptions
[modifier | modifier le wikicode]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 fenêtrage de registres
[modifier | modifier le wikicode]
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.

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.

L'interface de communication avec la mémoire
[modifier | modifier le wikicode]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.

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

Les registres d'interfaçage mémoire
[modifier | modifier le wikicode]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.

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.
L'unité de calcul d'adresse
[modifier | modifier le wikicode]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.

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 | modifier le wikicode]Il y a quelques chapitres, nous avons vu que certains processeurs ont des registres séparés pour les entiers et les adresses. Dans ce cas, le processeur incorpore un banc de registre séparé pour les registres d'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.

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 | modifier le wikicode]L'interface mémoire 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, 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.
En clair, détecter les accès non-alignés demande de tester si les bits de poids faibles adéquats sont à 0. Il suffit donc d'un circuit de comparaison avec zéro; qui est une simple porte OU. Cette porte OU génère un bit 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 | modifier le wikicode]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 | modifier le wikicode]L'organisation interne du processeur dépend fortement des modes d'adressage supportés. Pour simplifier les explications, nous allons séparer les modes d'adressage qui gèrent les pointeurs et les autres. Suivant que le processeur supporte les pointeurs ou non, l'organisation des bus interne est légèrement différente. La différence se voit sur les connexions avec le bus d'adresse et de données.
L'implémentation du processeur dépend de si les instructions peuvent effectuer plusieurs accès mémoire ou non, par exemple pour lire deux opérandes en mémoire RAM. L'implémentation est plus simple si on interdit ce genre d'instructions, vu que le bus mémoire ne gère qu'un seul transfert à la fois. Supporter les instructions multi-accès est plus compliqué. Par exemple, faire deux accès consécutives à deux adresses différentes pour charger deux opérandes depuis la RAM. Ce séquençage des accès mémoire pour une seule instruction est assez complexe. Il demande de modifier le séquenceur et d'ajouter des registres internes au processeur qui sont cachés du programmeur.
Introduction propédeutique : l'implémentation des modes d'adressage principaux
[modifier | modifier le wikicode]Tout processeur gère au minimum le mode d'adressage absolu, où l'adresse est intégrée à l'instruction. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Pour cela, le séquenceur est relié au bus d'adresse, le chemin de donnée est relié au bus de données. Le chemin de donnée n'est pas connecté au bus d'adresse, il n'y a pas d'autres connexions.

Le support des pointeurs demande d'intégrer des modes d'adressage dédiés : l'adressage indirect à registre, l'adresse base + indice, et les autres. Les pointeurs sont stockés dans le banc de registre et sont modifiés par l'unité de calcul. Pour supporter les pointeurs, le chemin de données est connecté sur le bus d'adresse avec le séquenceur. Suivant le mode d'adressage, le bus d'adresse est relié soit au chemin de données, soit au séquenceur.

Pour terminer, il faut parler des instructions de copie mémoire vers mémoire, qui copient une donnée d'une adresse mémoire vers une autre. Elles ne se passent pas vraiment dans le chemin de données, mais se passent purement au niveau des registres d’interfaçage. L'usage d'un registre d’interfaçage unique permet d'implémenter ces instructions très facilement. Elle se fait en deux étapes : on copie la donnée dans le registre d’interfaçage, on l'écrit en mémoire RAM. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.
Le banc de registre est multi-port, pour gérer nativement les opérations dyadiques
[modifier | modifier le wikicode]Les architectures RISC et CISC incorporent un banc de registre, qui est connecté aux unités de calcul et au bus mémoire. Et ce banc de registre peut être mono-port ou multiport. S'il a existé d'anciennes architectures CISC utilisant un banc de registre mono-port, elles sont actuellement obsolètes. Nous les aborderons dans un chapitre dédié aux anciennes architectures, mais nous pouvons les laisser de côté pour le moment. De nos jours, tous les processeurs utilisent un banc de registre multi-port.

Le banc de registre multiport est optimisé pour les opérations dyadiques. Il dispose précisément de deux ports de lecture et d'un port d'écriture pour l'écriture. Un port de lecture par opérande et le port d'écriture pour enregistrer le résultat. En clair, le processeur peut lire deux opérandes et écrire un résultat en un seul cycle d'horloge. L'avantage est que les opérations simples ne nécessitent qu'une micro-opération, pas plus.

Une architecture LOAD-STORE basique, avec adressage absolu
[modifier | modifier le wikicode]Voyons maintenant comment l'implémentation d'une architecture RISC très simple, qui ne supporte pas les adressages pour les pointeurs, juste les adressages inhérent (à registres) et absolu (par adresse mémoire). Les instructions LOAD et STORE utilisent l'adressage absolu, géré par le séquenceur, reste à gérer l'échange entre banc de registres et bus de données. Une lecture LOAD relie le bus de données au port d'écriture du banc de registres, alors que l'écriture relie le bus au port de lecture du banc de registre. Pour cela, il faut ajouter des multiplexeurs sur les chemins existants, comme illustré par le schéma ci-dessous.

Ajoutons ensuite les instructions de copie entre registres, souvent appelées instruction COPY ou MOV. Elles existent sur la plupart des architectures LOAD-STORE. Une première solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.

Mais il existe une seconde solution, qui ne demande pas de modifier le chemin de données. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU une opération Pass through, à savoir qu'elle recopie une des opérandes sur sa sortie. Le fait qu'une ALU puisse effectuer une opération Pass through permet de fortement simplifier le chemin de donnée, dans le sens où cela permet d'économiser des multiplexeurs. Mais nous verrons cela sous peu. D'ailleurs, dans la suite du chapitre, nous allons partir du principe que les copies entre registres passent par l'ALU, afin de simplifier les schémas.
L'ajout des modes d'adressage indirects à registre pour les pointeurs
[modifier | modifier le wikicode]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.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre, où l'adresse à lire/écrire est dans un registre. L'implémenter demande donc de connecter la sortie du banc de registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un port de lecture.

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 donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.

Le chemin de données précédent gère 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.
Le schéma précédent montre que le bus d'adresse est connecté à un MUX avant l'ALU et un autre MUX après. Mais il est possible de se passer du premier MUX, utilisé pour le mode d'adressage indirect à registre. La condition est que l'ALU supporte l'opération pass through, un NOP, qui recopie une opérande sur sa sortie. L'ALU fera une opération NOP pour le mode d'adressage indirect à registre, un calcul d'adresse pour le mode d'adressage base + indice. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.

Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent, afin d'avoir des schéma plus lisibles.
L'adressage immédiat et les modes d'adressages exotiques
[modifier | modifier le wikicode]Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. La constante est extraite de l'instruction par le séquenceur, puis insérée au bon endroit dans le chemin de données. 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 circuit spécialisé.

L'implémentation précédente gère aussi les modes d'adressage base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constante 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 absolu 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.

Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a rien à faire si l'unité de calcul est capable d'effectuer une opération NOP/pass through. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouve dans les registres. 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, à travers un MUX dédié.

Les architectures CISC : les opérations load-op
[modifier | modifier le wikicode]Tout ce qu'on a vu précédemment porte sur les processeurs de type LOAD-STORE, souvent confondus avec les processeurs de type RISC, où les accès mémoire sont séparés des instructions utilisant l'ALU. Il est maintenant temps de voir les processeurs CISC, qui gèrent des instructions load-op, qui peuvent lire une opérande depuis la mémoire.
L'implémentation des opérations load-op relie le bus de donnée directement sur une entrée de l'unité de calcul, en utilisant encore une fois un multiplexeur. L'implémentation parait simple, mais c'est parce que toute la complexité est déportée dans le séquenceur. C'est lui qui se charge de détecter quand la lecture de l'opérande est terminée, quand l'opérande est disponible.
Les instructions load-op s'exécutent en plusieurs étapes, en plusieurs micro-opérations. Il y a typiquement une étape pour l'opérande à lire en mémoire et une étape de calcul. L'usage d'un registre d’interfaçage permet d'implémenter les instructions load-op très facilement. Une opération load-op charge l'opérande en mémoire dans un registre d’interfaçage, puis relier ce registre d’interfaçage sur une des entrées de l'ALU. Un simple multiplexeur suffit pour implémenter le tout, en plus des modifications adéquates du séquenceur.

Annexe : le clock/power gating du chemin de données
[modifier | modifier le wikicode]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 | modifier le wikicode]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 peut désactiver l'ALU une fois l'adresse calculée, ce qui la désactive 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 | modifier le wikicode]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.