Fonctionnement d'un ordinateur/L'unité de contrôle

Il est maintenant temps de voir l'unité de contrôle. Pour rappel, celle-ci s'occupe du chargement des instructions et de leur interprétation. Elle contient l'unité de chargement, qui charge l'instruction depuis la mémoire, et le séquenceur, qui commande le chemin de données.

L’étape de chargementModifier

L'étape de chargement (ou fetch) est toujours décomposée en trois étapes :

  • la mise à jour du program counter (parfois faite en parallèle de la seconde étape).
  • l'envoi du contenu du program counter sur le bus d'adresse ;
  • la lecture de l'instruction sur le bus de données ;

À chaque chargement, le program counter est mis à jour afin de pointer sur la prochaine instruction à charger. Et la méthode pour ce faire dépend du processeur. Sur certains processeurs assez rares, chaque instruction précise l'adresse de la suivante, qui est incorporée dans l'instruction. Mais sur la majorité des processeurs, on profite du fait que les instructions sont placées dans l'ordre d’exécution dans la RAM. En faisant ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction courante au contenu du program counter. L'adresse de l'instruction en cours est connue dans le program counter, reste à en connaitre la longueur. Le calcul est simple quand les instructions ont toutes la même taille, mais certains processeurs ont des instructions de taille variable, ce qui complique le calcul.

Le registre d'instructionModifier

Sur certains processeurs, l'instruction chargée est parfois stockée dans un registre situé juste avant le séquenceur. Ce registre est appelé le registre d'instruction.

 
Registre d'instruction.

Les processeurs basés sur une architecture Harvard peuvent se passer de ce registre, vu que l'instruction reste disponible sur le bus des instruction pendant toute son exécution. Mais sur les architectures de type Von Neumann, le bus doit être libéré pour un éventuel accès mémoire pendant l'instruction. On doit donc charger l'instruction et accéder aux données en deux temps : on charge l'instruction d'abord, puis on accède aux données. Mais cela demande de mémoriser l'instruction en cours dans le processeur, pour libérer le bus, d'où l'existence du registre d'instruction.

Le program counterModifier

Dans la quasi-totalité des processeurs modernes, le program counter est séparé des autres registres et est contrôlé par des circuits dédiés. Le program counter est un vulgaire compteur comme on en a vu dans les chapitres sur les circuits, ce qui fait qu'il passe automatiquement d'une adresse à la suivante. Sur les processeurs très anciens, le program counter était un simple incrémenteur, car ces processeurs arrivaient à caler leurs instructions sur un seul mot mémoire.

Sur les processeurs modernes, on n'utilise pas exactement un circuit compteur comme on en a vu dans les chapitres sur les circuits. A la place, le program counter est relié à un circuit qui calcule l'adresse de la prochaine instruction, circuit appelé le compteur ordinal. Le compteur ordinal contient un additionneur et quelques circuits annexes pour gérer les branchements. L'usage d'un compteur ordinal simplifie fortement l'architecture du processeur et la conception du séquenceur. C'est une méthode simple et facile à implémenter. De plus, avec elle, le séquenceur n'a pas à gérer la mise à jour du program counter, sauf en cas de branchements. Un autre avantage est que le program counter peut être mis à jour pendant que l'unité de calcul effectue une opération arithmétique, ce qui améliore grandement les performances. Le seul désavantage est qu'elle demande d'ajouter des circuits, dont un additionneur

 
Étape de chargement.

Mais sur certains processeurs, le calcul de la prochaine adresse est effectué par l'ALU. Après tout, celle-ci est capable d'effectuer une addition, ce qui l'opération demandée pour mettre à jour le program counter. L'avantage de cette méthode est qu'elle économise des circuits : on économise un additionneur et quelques circuits annexes. Par contre, cela se paye au prix d'une augmentation de la complexité du séquenceur, qui doit maintenant gérer la mise à jour du program counter. Un autre défaut est que la mise à, jour du program counter ne peut pas se faire en même temps qu'une opération arithmétique, ce qui réduit les performances. Cette méthode était utilisée sur certains processeurs assez anciens, où l'économie de circuit était primordiale. Il faut dire que les microprocesseurs de l'époque n'avaient qu'un nombre limité de transistors, la technologie des semi-conducteurs ne permettant pas de mettre beaucoup de transistors sur une puce électronique. Et un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Autant dire que les processeurs de l'époque gagnaient à utiliser cette technique.

Sur certains de ces processeurs, le program counter n'est pas séparé du reste des autres registres. Le program counter est alors placé dans le banc de registre, avec les registres pour les nombres entiers. Mettre le program counter dans le banc de registres n'est pas une obligation, c'est juste plus simple pour utiliser l'ALU. L'avantage est que la conception du processeur est la plus simple possible. Par contre, on perd un registre. Par exemple, si on a un banc de registre de 16 registres, on ne peut utiliser que 15 registres généraux. Non seulement on perd un registre, mais en plus ce n'est pas l'idéal pour le décodage des instructions et pour le séquenceur.

D'autres processeurs anciens utilisent une solution intermédiaire, hybride. Ils ont bien un registre séparé pour le program counter, mais ils utilisent l'ALU pour mettre à jour le program counter. Cette méthode est relativement simple à implémenter. Il suffit de connecter/déconnecter le program counter du bus interne suivant les besoins. Le program counter est déconnecté pendant l'éxecution d'une instruction, mais il est connecté au bus interne lors de sa mise à jour. C'est le séquenceur qui gère le tout. Outre sa simplicité, l'avantage de cette séparation est qu'elle est plus facile à gérer pour le séquenceur. De plus, on gagne un registre général/entier dans le banc de registre. Un exemple de cette méthode est le processeur 6502, dont le chemin de données est illustré ci-contre.

Pour résumer, il existe trois méthodes principales pour calculer l'adresse de la prochaine instruction, l'une étant une sorte d'intermédiaire entre les deux autres. Elles se distinguent sur deux points : le circuit qui fait le calcul, l'organisation des registres. L'implémentation du circuit de calcul est sujet à deux possibilités : soit le program counter a son propre additionneur, soit le program counter est mis à jour par l'ALU. Pour ce qui est de l'organisation des registres, soit le program counter est un registre séparé des autres, soit il est regroupé avec d'autres registres dans un banc de registre. Le fait de placer le program counter dans un banc de registre implique que celui-ci soit mis à jour par l'ALU. PAr contre, on peut avoir un program counter séparé des autres registres, mais qui est mis à jour par l'ALU. Au total, cela donne les trois solutions suivantes.

 
les différentes méthodes de calcul du program counter.

À noter que la seconde solution est illustrée dans le cas où on a un seul banc de registre banalisé, qui contient des registres généraux. Mais elle doit être adaptée sur les processeurs qui ont des registres séparés pour les adresses mémoires ou la gestion de la pile.

Un premier cas est celui où le processeur dispose de registres spécialisés pour la gestion de la pile d'appel. Typiquement, un processeur de ce type a un registre pour le pointeur de pile, éventuellement un autre pour le frame pointer. Or, mettre à jour le pointeur de pile demande d'effectuer une addition ou une soustraction. Elle est réalisée par l'ALU sur la plupart des processeurs, surtout sur les processeurs où le pointeur de pile est placé dans le banc de registre. Mais sur d'autres processeurs, le pointeur de pile est regroupé avec le program counter et les deux sont mis à jour par le même additionneur. C'est là une utilisation intelligente de l'additionneur, même si cela impose quelques contraintes sur la taille des cadres de pile (qui doivent être de taille fixes). Cette technique marche bien si les instructions et les cadres de pile ont la même taille. Typiquement, si un cadre de pile a la même taille qu'un entier et que les entiers ont la même taille qu'une instruction, cette solution peut être envisagée. Mais si les cadres de pile sont de taille variable, ou qu'ils n'ont pas la même taille qu'une instruction, alors cette solution ne marche pas.

Un exemple similaire est celui du processeur 80186, où les registres d'adresses dédiés à la segmentation (une technique de mémoire virtuelle dont nous parlerons vers le milieu du cours) sont regroupés avec le program counter et partagent un additionneur.

Un second cas est celui des processeurs avec un banc de registres et une ALU spécialisée pour les adresses. Il y a quelques chapitres, nous avons vu que certains processeurs ont des registres séparés pour les entiers et les adresses, ou utilisent des registres d'index séparés des registres entiers. Typiquement, le processeur incorpore alors un banc de registre pour les adresses/indices, et un autre banc de registres pour les entiers. De tels processeurs avaient souvent deux unités de calculs : une pour les calculs entiers, et un autre pour les calculs d'adresse. La seconde ALU était plus simple, et se résumait souvent à un vulgaire incrémenteur/décrémenteur. Sur de tels processeurs, le program counter était placé dans le banc de registre pour les adresses et était mis à jour par l'ALU spécialisée dans les calculs d'adresse. 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.

 
Calcul du program counter avec des registres d'adresse sécialisés

Enfin, il est des processeurs pour lesquels l'organisation est simplement déroutante. Le Z80 est un processeur dans ce style. Il dispose d'un circuit incrémenteur séparé de l'ALU, qui agit comme une ALU secondaire. Le circuit incrémenteur est utilisé pour mettre à jour le program counter, mais aussi pour mettre à jour le pointeur de pile et pour le rafraichissement mémoire. De plus, il est utilisé pour les instructions INC et DEC qui incrémentent un registre de 16 bits. Pour être plus précis, le Z-80 a des registres de 8 bits, mais qui sont regroupés par paires. Certaines instructions manipulent des registres de 8 bits isolés, alors que d'autres, comme INC et DEC, prennent une paire de registres comme un registre de 16 bit. En clair, on a une ALU principale connectée uniquement aux registres entiers, accompagnée d'un incrémenteur relié à tous les registres (entier, pointeur de pile, program counter, ...).

Au niveau du jeu d'instruction, le program counter semble regroupé avec les autres registres. Mais l'étude du silicium du processeur montre que ce n'est pas le cas. Le program counter est regroupé avec un registre utilisé pour le rafraichissement mémoire, vu que c'était le processeur qui s'en occupait à l'époque. Ce registre, nommé IR, mémorise la prochaine adresse mémoire à rafraichir. Les deux registres sont connectés au banc de registre avec un bus interne au processeur, ce qui permet de copier un registre dans le program counter, afin d'effectuer un branchement indirect. Mais le bus est déconnecté en dehors d'un branchement indirect, ce qui fait que la mise à jour du program counter se fait en parallèle des calculs arithmétiques. On a donc deux bancs de registres : un avec les entiers et le pointeur de pile, un autre avec le program counter et un registre pour le rafraichissement mémoire. Ces deux derniers sont les principaux utilisateurs de l'incrémenteur, mais ils ne sont pas les seuls.

 
Architecture du Z80, telle que décrite par le jeu d’instruction. Elle ne correspond pas à l'organisation interne réelle du processeur. Notamment, l'incrémenteur (noté +1 sur le schéma) est représenté en double, alors que l'étude du die du processeur montre qu'il n'y en a qu'un seul. De même, l'organisation des registres est différente de celle montrée sur ce schéma.

Le chargement des instructions de longueur variableModifier

Le chargement des instructions de longueur variable est assez compliqué. Le problème est que mettre à jour le program counter demande de connaitre la longueur de l'instruction chargée, pour l'ajouter au program counter. Si la longueur d'une instruction est variable, on doit connaitre cette longueur d'une manière où d'une autre.

La plus simple consiste à indiquer la longueur de l'instruction dans une partie de l'opcode ou de la représentation en binaire de l'instruction.

Une autre solution consiste à charger l'instruction morceau par morceau, typiquement par blocs de 16 ou 32 bits. Ceux-ci sont alors accumulés les uns à la suite des autres dans le registre d'instruction, jusqu'à ce que le séquenceur reconstitue une instruction complète. Le seul défaut de cette approche, c'est qu'il faut détecter quand une instruction complète à été reconstituée. Une solution similaire permet de se passer d'un registre d'instruction, en transformant le séquenceur en circuit séquentiel. Les mots mémoire de l'instruction sont chargés à chaque cycle, faisant passer le séquenceur d'un état interne à un autre à chaque mot mémoire. Tant qu'il n'a pas reçu tous les mots mémoire de l'instruction, chaque mot mémoire non terminal le fera passer d'un état interne d'attente à un autre. S'il tombe sur le dernier mot mémoire d'une instruction, il rentre dans un état de décodage.

Et enfin, il existe une dernière solution, qui est celle qui est utilisée dans les processeurs haute performance de nos PC : charger un bloc de mots mémoire qu'on découpe en instructions, en déduisant leurs longueurs au fur et à mesure. Généralement, la taille de ce bloc est conçue pour être de la même longueur que l'instruction la plus longue du processeur. Ainsi, on est sûr de charger obligatoirement au moins une instruction complète, et peut-être même plusieurs. Cette solution marche parfaitement si les instructions sont alignées en mémoire (relisez le chapitre sur l'alignement et le boutisme si besoin). Par contre, elle pose problème avec des instructions à cheval entre deux blocs. Il se peut qu'on n'ait pas une instruction complète lorsque l'on arrive à la fin du bloc, mais seulement un morceau.

 
Instructions non alignées.

Dans ce cas, on doit décaler le morceau de bloc pour le mettre au bon endroit (au début du registre d'instruction), et charger le prochain bloc juste à côté. On a donc besoin d'un circuit qui décale tous les bits du registre d'instruction, couplé à un circuit qui décide où placer dans le registre d'instruction ce qui a été chargé, avec quelques circuits autour pour configurer les deux circuits précédents.

 
Décaleur d’instruction.

Le CPU reset vectorModifier

Au démarrage d'un ordinateur, le program counter est initialisé avec l'adresse de la première instruction. Pour déterminer l'adresse de démarrage, on a deux solutions : soit on utilise une adresse fixée une fois pour toute, soit l'adresse peut être précisée par le programmeur en la plaçant en mémoire ROM.

  • Avec la première solution, la plus simple, le processeur démarre l’exécution du code à une adresse bien précise, toujours la même, câblée dans ses circuits. On peut par exemple faire démarrer le processeur à l'adresse 0 : le processeur lira le contenu de la toute première adresse et démarrera l’exécution du programme à cet endroit.
  • Avec la seconde solution, on ajoute un niveau de redirection. Le processeur est toujours conçu de manière à accéder à une adresse bien précise au démarrage. Mais l'adresse en question ne contient pas la première instruction à exécuter. A la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la charge dans son program counter. En clair, il effectue un branchement.

Dans tous les cas, le processeur est câblé pour lire une adresse bien précise lors du boot. Naturellement, cette adresse est appelée l'adresse de démarrage. En anglais, elle est appelée le CPU Reset vector. L'adresse de démarrage n'est pas toujours l'adresse 0 : les premières adresses peuvent être réservées pour la pile ou le vecteur d'interruptions. Les deux solutions évoquées plus haut interprètent différemment l'adresse de démarrage. C'est l'adresse de la première instruction avec la première solution, c'est un pointeur vers la première instruction dans l'autre.

Le séquenceurModifier

Pour rappel, le chemin de données est rempli de composants à configurer d'une certaine manière pour exécuter une instruction. Pour configurer le chemin de données, il faut envoyer les signaux de commande adéquats sur l'entrée de sélection de l'unité de calcul, les entrées du banc de registres, ou les circuits du bus du processeur. Lorsque les circuits du chemin de données reçoivent ces signaux de commande, ils sont conçus pour effectuer une action précise et déterminée. Mais l'instruction chargée depuis la mémoire ne précise pas les signaux de commande, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une série de signaux de commandes adéquats. C'est le rôle de l'unité de décodage d'instruction, une portion du séquenceur qui « décode » l'instruction. Le séquenceur traduit une instruction en suite de micro-opérations et émet les signaux de commande pour chaque micro-opération.

Il existe des processeurs assez rares où chaque instruction machine précise directement les signaux de commande, sans avoir besoin d'une unité de décodage d'instruction. L'encodage de l'instruction en mémoire est alors très simple : il suffit de placer les signaux de commande les uns à la suite des autres, rien de plus. De telles architectures sont appelées des architectures actionnées par déplacement. Elles feront l'objet d'un chapitre dédié à la fin du cours. En attendant, nous allons mettre ces architectures de côté pour le moment et nous concentrer sur des architectures plus courantes.
 
Unité de décodage d'instruction

Mais la traduction en question n'est pas simple, pour une raison assez importante : une instruction peut correspondre à plusieurs actions différentes sur le chemin de données. Nous avions vu il y a quelques chapitres que les instructions sont décomposées en plusieurs étapes, appelées micro-instructions, chacune configurant le chemin de donnée d'une manière bien précise. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Dans le cas le plus simple, chaque instruction correspond à une micro-opération et la traduction est alors triviale : le circuit est un simple circuit combinatoire. Mais dès que ce n'est pas le cas, le séquenceur devient un circuit séquentiel avec toute la complexité que cela implique.

Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Celui-ci est un circuit séquentiel, c'est à dire qu'il contient un circuit combinatoire et des registres. Or, tout circuit combinatoire peut être remplacé par une mémoire ROM au contenu adéquat, les deux étant totalement équivalents. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés : une partie des instructions est décodée par une partie câblée, et l'autre par une partie microcodée. Ils permettent de profiter des avantages des deux types de séquenceurs. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par la mémoire ROM, plus lente.

Les séquenceurs câblésModifier

Si les instructions sont décodées par un circuit combinatoire couplé à des registres, on parle de séquenceur câblé. Plus le nombre d'instructions à câbler est important, plus le séquenceur utilise de portes logiques pour sa conception. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides. Par contre, les séquenceurs câblés sont souvent utilisés sur les processeurs RISC, qui ont peu d'instructions, pour lesquels la complexité du séquenceur et le nombre de portes est assez faible et supportable. La complexité du séquenceur dépend de la complexité des instructions machine.

Les séquenceurs combinatoiresModifier

Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela nécessite des conditions assez particulières. Tout d'abord, la mémoire dans laquelle sont stockées les instructions doit être physiquement séparée de la mémoire dans laquelle on stocke les données, afin de pouvoir accéder en même temps à la mémoire programme (pour charger l'instruction) et aux données (pour exécuter les instructions d'accès mémoire). Ensuite, chaque instruction du processeur ne doit effectuer qu'une seule modification du chemin de données. Dans une telle situation, le processeur effectue chaque instruction en un seul cycle d'horloge. Mais autant le dire tout de suite : ces processeurs ne sont pas vraiment pratiques. Avec eux, un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente.

 
Séquenceur combinatoire

Les séquenceurs séquentielsModifier

Disposer d'instructions prenant des temps variables permet d'éviter les problèmes des architectures précédentes : au lieu que toutes les instructions soient lentes, il vaut mieux avoir des instructions rapides et d'autres lentes. Et la manière la plus simple de faire cela est d'avoir des instructions découpées en plusieurs micro-opérations. Et c'est le cas sur la majorité des processeurs, où les instructions s’exécutent en plusieurs micro-opérations. Pour enchaîner les micro-opérations, le séquenceur doit savoir à quelle micro-opération il en est, et mémoriser cette information dans une mémoire interne. En conséquence, ces séquenceurs câblés sont des circuits séquentiels, basés sur un circuit combinatoire couplé à un ou des registres.

 
Séquenceur séquentiel

Une conséquence est que les instructions ont une durée d’exécution variable. Pour gérer les instructions qui prennent plusieurs cycles d'horloge, le séquenceur doit éviter de changer d'instruction à chaque cycle : le processeur doit autoriser ou interdire les modifications du program counter tant qu'il est en train de traiter une instruction.

 
Commande de la mise à jour du program counter.

Les séquenceurs microcodésModifier

Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les séquenceurs microcodés. l'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le control store, tandis que son contenu s'appelle le microcode.

Les séquenceurs micro-codés sont plus simples à concevoir et cela simplifie beaucoup le travail des concepteurs de processeurs. L'usage du microcode a un autre avantage majeur : il permet d'ajouter des instructions facilement sans pour autant modifier en profondeur le processeur. Ajouter des instructions sur une nouvelle génération de processeur demande juste d'ajouter celles-ci dans le microcode, ce qui est trivial. les instructions microcodées en question ne seront pas forcément plus rapides que leur équivalent logiciel, mais les possibilités d'optimisation sont quand même là. Cet avantage a surtout été utilisé sur les processeurs CISC, mais il n' a plus trop d'intérêt de nos jours. En contrepartie de sa simplicité de conception, un séquenceur microcodé a des inconvénients qui ne sont pas négligeables. Par exemple, ils utilisent plus de portes logiques que les séquenceurs câblés, vu qu'une ROM est un circuit gourmand en portes logique. De plus, un séquenceur micro-codé est plus lent qu'un séquenceur câblé. L raison à cela est qu'une mémoire ROM est bien plus lente qu'un circuit combinatoire fabriqué directement avec des portes logiques. Cela se ressent sur la fréquence d'horloge du processeur.

Le control store et les circuits d’exécution du microcodeModifier

Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont stockées les micro-opérations.

  • Avec le microcode horizontal, chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu e grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits !
  • Avec un microcode vertical, les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé suit suit le control store. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un control store plus petit ou d'avoir un microcode plus important (donc plus d'instructions machines microcodées).

Pour retrouver la suite de micro-opérations correspondante, le séquenceur considère l'opcode de l'instruction microcodée comme une adresse : le control store est conçu pour que cette adresse pointe directement sur la suite de micro-opérations correspondante. Plus précisément, elle pointe vers la première micro-instruction.

 
Control store d'un microcode horizontal.

Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations.

Pour cela, il y a deux méthodes. La première, assez rare, est que chaque micro-instruction contient l'adresse de la micro-instruction suivante.

 
Microcode sans microséquenceur.

Dans la majorité des cas, le séquenceur contient un équivalent du program counter pour le microcode : le registre d’adresse de micro-opération, couplé à un microcompteur ordinal. Ce registre est initialisé avec l'opcode instruction à exécuter, qui pointe vers la première micro-instruction.

 
Microcode avec un microséquenceur.

Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille.

Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement.

 
Branchements avec microcode horizontal avec microséquenceur.

Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante :

 
Branchements avec microcode horizontal sans microséquenceur.

La mise à jour du microcodeModifier

Parfois, le processeur permet une mise à jour du control store, ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de faire des corrections de bugs ou de corriger des problèmes de sécurité assez tordus. Il est en effet fréquent que les processeurs soient sujets à des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans une documentation officielle assez imposante, preuve que ces bugs ne sont pas des exceptions d'une grande rareté. Si la plupart de ces bugs ne peuvent pas être corrigés, quelques bugs peuvent cependant se corriger avec des mises à jour du microcode interne au processeur. Les bugs en question peuvent être liés à des bugs dans le microcode lui-même, ou à des bugs situés ailleurs, mais qui peuvent être corrigés ou mitigés en bidouillant le microcode. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert.

Mais il existe des processeurs dont le microcode est facilement programmable, accessible par le programmeur, ce qui permet d'ajouter des instructions à la volée. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction. Cela permet de réduire le nombre d'instructions du programme, ce qui réduit la taille du code (la mémoire prise par le programme exécutable), mais facilite aussi la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un control store assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité pose des problèmes : si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui n'ont pas intérêt à utiliser des instructions microcodées, sous peine d'exécuter des instructions microcodées incorrectes. Les problèmes de compatibilité entre processeurs sont aussi légion (les programmes codés ainsi ne marchent que sur un seul processeur, pas les autres). Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Aussi, il n'est pas étonnant que les microcodes nus, facilement accessibles, sont très très rares. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastique intégrées au processeur (microcode fournit chiffré avec des clés connues seulement des fabricants de CPU, autres).

La mise à jour du microcode peut se faire de deux grandes manières, l'une étant permanente, l'autre étant temporaire. La mise à jour permanente du microcode signifie que le control store est une EEPROM ou une mémoire ROM reprogrammable. Mais c'est une solution rarement utilisée, car ces mémoires sont très difficiles à mettre en œuvre dans les processeurs. Le control store doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. Les mémoires mask ROM, non-reprogrammables, rentrent clairement dans ce cadre car elles sont fabriquées avec les mêmes transistors que le reste du processeur. Mais ce n'est pas le cas des autres mémoires ROM basées sur des transistors à grille flottante, totalement différents des transistors normaux. Il faut donc trouver une autre solution.

L'autre solution permet de mettre à jour le control store temporairement. Pour cela, le control store est implémenté avec deux mémoires. Une mémoire ROM qui contient le microcode originel, et une mémoire RAM. Pour simplifier ls explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. La micro-RAM est utilisée lors du décodage des instructions. L'idée est de permettre de modifier le microcode dans la micro-RAM avec un microcode corrigé. Ce dernier est chargé dans le processeur, dans la micro-RAM, peu après l'allumage du processeur. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Des mises à jour du BIOS peuvent contenir un microcode corrigé, capable de corriger des bugs ou des failles de sécurité.

La gestion des branchementsModifier

L'implémentation des branchements est assez facile, mais elle implique tout le séquenceur. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le program counter quand un branchement est détecté. Pour cela, on doit ajouter un circuit de détection des branchements. Celui-ci détecte si l'instruction exécutée est un branchement ou non. Mais ce circuit est situé dans le décodeur d'instruction, c'est à dire dans la partie du séquenceur qui est située après l'étape de chargement. Autant dire que l’exécution des branchements demande une coopération de l'étape de chargement et le décodeur d'instruction. Aussi, nous allons voir le cas des branchements maintenant, à part des sections sur l'étape de chargement et de décodage d’instruction.

L'implémentation des branchements en fonction de leur mode d'adressageModifier

Pour rappel, il existe plusieurs types de branchements, et l'implémentation de chaque type est légèrement différente. Au niveau de l'étape de chargement, on ne fait pas de différence entre branchements inconditionnels (toujours exécutés) et conditionnels (exécutés si une condition précise est remplie). Par contre, il faut tenir compte du mode d'adressage du branchement. Pour les modes d'adressage, on distingue quatre types de branchements : les branchements directs, indirects, relatifs et implicites. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le reste du séquenceur. Pour les branchements indirects, l'adresse de destination est dans un registre. Pour les branchements relatifs, il faut ajouter un décalage au program counter. Enfin, les instructions de retour de fonction lisent l'adresse de destination depuis la pile. L'implémentation de ces quatre types de branchements est très différente. De plus, tout dépend de si le program counter est dans un compteur séparé du reste des registres, ou s'il est dans le banc de registre.

L’implémentation des branchements avec un program counter intégré au banc de registresModifier

Le cas le plus simple est clairement celui où le program counter est intégré au banc de registre. Dans ce cas, branchements indirects, relatifs et directs sont simples à implémenter et tout se passe dans le chemin de données. L'étape de chargement n'est en soi pas concernée par la mise à jour du program counter, mais s'occupe juste de connecter celui-ci au bus d'adresse. Le calcul d'adresse est alors réalisé dans le chemin de données, branchement ou non.

Reste que les trois types de branchements s'implémentent différemment, dans le sens où le chemin de donnée est configuré différemment suivant le mode d'adressage.

  • Les branchements indirects consistent à copier un registre dans le program counter, ce qui revient simplement à faire une opération MOV entre deux registres, la seule différence étant que le program coutner n'est pas adressable.
  • Les branchements directs et relatifs sont traités comme des opérations en mode d'adressage immédiat.
    • Pour les branchements directs, on utilise l'adressage direct. Concrètement, l'adresse de destination du branchement est directement écrite dans le banc de registre. l'opération est alors équivalent à une opération MOV avec une constante immédiate.
    • Les branchements relatifs demandent de lire le program counter depuis le banc de registre, d'ajouter une constante immédiate dans l'ALU, et d'écrire le tout dans le banc de registre. Au final, l’opération est juste une opération arithmétique avec une opérande lue depuis les registres et une autre en adressage immédiat.
 
Mise à jour du program counter avec branchements, si PC dans le banc de registres

Notons que sur certaines architectures, le program counter est non seulement dans le banc de registres, mais il est adressable au même titre que les registres de données. C'est étrange, mais cela permet de se passer d'instructions de branchement. Pour cela, il suffit d'adresser le program counter avec des instructions MOV ou des instructions arithmétique. Une simple instruction MOV reg -> program counter fait un branchement indirect, une instruction MOV constante -> program counter fait un branchement direct, et une instruction ADD constante program counter -> program counter fait un branchement relatif.

L’implémentation des branchements avec un program counter séparé, mis à jour par l'ALUModifier

Le second cas que nous allons voir est celui où le program counter est séparé du banc de registres, mais qu'il est incrémenté par l'ALU. Sur ce genre d'architectures, la gestion des branchements se fait d'une manière fortement similaire à ce qu'on a vu dans la section précédente. Là encore, l'ALU est utilisée pour incrémenter le program counter sans branchements, mais elle est aussi réutilisée pour les branchements relatifs. La seule différence est que le program counter est séparé du banc de registres.

 
Mise à jour du Program Counter par l'ALU en cas de branchements

L’implémentation des branchements avec un program counter séparéModifier

Étudions d'abord le cas où le program counter est dans un registre/compteur séparé. Rappelons que tout compteur digne de ce nom possède une entrée de réinitialisation, qui remet le compteur à zéro. De plus, on peut préciser à quelle valeur il doit être réinitialisé. Ici, la valeur à laquelle on veut réinitialiser le compteur n'est autre que l'adresse de destination du branchement. Implémenter un branchement est donc simple : l'entrée de réinitialisation est commandé par le circuit de détection des branchements, alors que la valeur de réinitialisation est envoyée sur l'entrée adéquate. En clair, nous partons du principe que le program counter est implémenté avec ce type de compteur :

 
Fonctionnement d'un compteur (décompteur), schématique

Toute la difficulté est de présenter l'adresse de destination au program counter. Pour les branchements directs, l'adresse de destination est fournie par le séquenceur. L'adresse de destination du branchement sort du séquenceur et est présentée au program counter sur l'entrée adéquate. Voici donc comment sont implémentés les branchements directs avec un program counter séparé du banc de registres. Le schéma ci-dessous marche peu importe que le program counter soit incrémenté par l'ALU ou par un additionneur dédié.

 
Unité de détection des branchements dans le décodeur

Les branchements relatifs sont ceux qui demandent de sauter X instructions plus loin dans le programme. En clair, ils ajoutent un décalage au program counter. Leur implémentation demande d'ajouter une constante au program counter, la constante étant fournie dans l’instruction. Pour prendre en compte les branchements relatifs, on a encore deux solutions : réutiliser l'ALU pour calculer l'adresse, ou rajouter un additionneur qui fait le calcul d'adresse. En théorie, l'additionneur ajouté peut être fusionné avec l'additionneur qui incrémente le program counter pour passer à l’instruction suivante, mais ce n'est pas toujours fait et on a parfois deux circuits séparés.

 
Unité de chargement qui gère les branchements relatifs.

Pour les branchements indirects, il suffit de lire le registre voulu et d'envoyer le tout sur l'entrée adéquate du program counter. Il faut alors rajouter un multiplexeur pour que l'entrée de réinitialisationr ecoive la bonne adresse de destination.

 
Unité de chargement qui gère les branchements directs et indirects.

Toute la difficulté de l'implémentation des branchements est de configurer les multiplexeurs, ce qui est réalisé par le séquenceur en fonction du mode d'adressage du branchement.

L'implémentation des branchements conditionnelsModifier

Les branchements inconditionnels s’exécutent systématiquement, alors que les branchements conditionnels demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels :

  • Ceux qui doivent être précédés d'une instruction de test ou de comparaison.
  • Ceux qui effectuent le test et le branchement en une seule instruction machine.
  • Ceux où les branchements conditionnels sont émulés par une skip instruction, une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel.
 
Implémentations possibles des branchements conditionnels.

Formellement, un branchement conditionnel demande de faire deux choses : calculer une condition, puis faire le branchement suivant le résultat de la condition. Dans ce qui suit, nous allons d'abord voir le cas où calcul de la condition et saut conditionnels sont réalisés tous deux par une seule instruction. Puis, nous verrons ensuite le cas où test et saut sont séparé dans deux instructions séparées. La raison est que le premier cas est le plus simple à implémenter. Le second cas demande d'ajouter des registres et quelques circuits, ce qui rend le tout plus compliqué.

Les circuits de saut conditionnel et de calcul de la conditionModifier

Le calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données. Premièrement, deux opérandes sont lues depuis les registres, puis sont envoyés à un circuit soustracteur. Le circuit soustracteur soustrait alors les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les bits intermédiaires, et ils sont combinés pour calculer les différentes conditions. En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égales, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs.

 
Calcul d'une condition pour un branchement

Outre le calcul de la condition, un branchement conditionnel demande de calculer une condition et de sauter ou non à une certaine adresse en fonction du résultat de cette dernière. En soi, le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du program counter et en mettant à 1 son entrée de réinitialisation. La seule difficulté consiste donc à mettre la bonne valeur sur l'entrée de réinitialisation. Le program counter doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : son résultat est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant.

 
Implémentation des branchements conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici.

Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand le processeur fournit des instructions où test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple, avec une belle séparation entre l'unité de calcul pour les tests, le séquenceur et l'unité de chargement. Les trois ont des rôles bien précis : calculer le test pour l'ALU dédiée, décider s'il faut faire le saut ou non et modifier le program counter pour l'unité de chargement, générer les signaux de commande pour l'ALU et l'unité de chargement pour le séquenceur. Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit.

 
Implémentation des branchements.

Le registre d'état ou les registres à prédicats et les circuits associésModifier

Voyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières.

  • La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat.
  • La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement.

Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change.

Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul.

 
Implémentation des branchements avec un registre d'état

L'autre cas est celui où les résultats des conditions sont mémorisés dans des registres à prédicats, connectés au séquenceur. Cela amène deux problèmes : l'instruction de test doit enregistrer le résultat dans le bon registre à prédicat, et il faut aussi lire le bon registre à prédicat suivant le branchement. Il faut donc gérer la sélection en lecture et en écriture. Rappelons que les registres à prédicats sont numérotés, ils ont un nom de registre dédié qui est fournit par le séquenceur. La sélection en lecture et écriture des registres à prédicat se base donc sur ces noms de registre. Pour la sélection en lecture, le choix du registre à prédicat voulu est réalisé par un multiplexeur, commandé par le séquenceur. Le multiplexeur est intégré à l'unité de chargement ou au séquenceur, peu importe. Pour l'enregistrement dans le bon registre à prédicat, le choix est réalisé en sortie de l'unité de calcul, généralement par un démultiplexeur.

 
Implémentation de l'unité de chargement avec plusieurs registres à prédicats

L'implémentation des skip instructionsModifier

Passons maintenant au cas des skip instruction, qui permettent d'émuler les branchements conditionnels par une instruction de test spéciale. Pour rappel, une skip instruction permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. . Dans ce cas, le program counter est incrémenté normalement si la condition n'est pas respectée, mais il est incrémenté deux fois si elle l'est. Les branchements inconditionnels s’exécutent normalement. Là encore, suivant la condition testée, on trouve un multiplexeur pour choisir le bon résultat de condition.

 
Implémentation des branchements pseudo-conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici, de même que le multiplexeur pour choisir la bonne condition.

L'implémentation des instructions à prédicatsModifier

Les instructions à prédicats sont des instructions qui s’exécutent seulement si une condition précise est remplie. Elles sont précédées d'une instruction de test qui met à jour le registre d'état ou un registre à prédicat. L'instruction à prédicat récupère alors le résultat de la condition, calculé par l'instruction de test précédente, et l'utilise pour savoir si elle doit se comporter comme un NOP ou si elle doit faire une opération. Leur implémentation est variable et deux grandes méthodes sont possibles. La première n’exécute pas l'instruction si la condition est invalide, l'autre l’exécute en avance mais n'enregistre pas son résultat dans les registres si la condition se révèle ultérieurement invalide.

La première méthode exécute l'opération, mais l'annule si la condition n'est pas respectée. Le calcul des conditions est fait en parallèle de l'autre opération et l'annulation se fait simplement en n'enregistrant pas le résultat de l’opération dans les registres. Le calcul de la condition s'effectue dans le séquenceur, mais le résultat est envoyé dans le chemin de données pour configurer un circuit qui autorise ou non l'enregistrement du résultat dans les registres. un défaut de cette technique est que l'instruction est effectivement exécutée, ce qui fait que le processeur a consommé un peu d'énergie et a pris un peu de temps pour faire le calcul. L'autre conséquence est que l'instruction mobilise une unité de calcul ou de transfert entre registre, le banc de registres, etc. En soi, ce n'est pas un problème. Mais ça l'est sur les processeurs modernes, qui sont capables d’exécuter plusieurs instructions en parallèle, dans ujn ordre différent de celui imposé par le programmeur. Nous verrons ces techniques d’exécution en parallèle dans les derniers chapitres du cours. Toujours est-il que sur ces processeurs, une instruction à prédicats va mobiliser des ressources matérielles comme l'ALU ou le bus interne, pour éventuellement fournir un résultat inutile, alors qu'une autre instruction aura pu prendre sa place et calculer des données utiles.

 
Implémentation des instructions à prédicats

La seconde méthode est la plus intuitive : elle consiste à lire le registre d'état/de prédicat, pour décider s'il faut faire ou non l'opération. Pour cela, le séquenceur lit le registre d'état/à prédicat, et génère les signaux de commande adaptés : il génère les signaux de commande d'un NOP si la condition n'est pas respectée, et il génère les signaux de commande pour l'opération voulue sinon. L’avantage de cette méthode est que l'instruction ne s’exécute pas si la condition n'est pas remplie. Le processeur ne gache pas d'énergie pour rien, il peut immédiatement passer à l'instruction suivante si celle-ci est disponible, etc. De plus, sur les processeurs modernes capables d’exécuter plusieurs instructions en parallèle, on ne mobilise pas de ressources matérielles si la condition n'est pas remplie et celles-ci sont disponibles pour d'autres instructions.

 
Implémentation des instructions à prédicats simples

La Prefetch input queueModifier

Sur certains processeurs, l'étage de chargement et le chemin de données sont séparés par une mémoire tampon, appelée la file d’instruction, aussi appelée Prefetch input queue. On peut la voir comme un registre d'instruction sous stéroïde, capable de mémoriser plusieurs instructions consécutives. Elle se situe après l'unité de chargement, même si on peut en théorie la mettre après l'unité de décodage. Elle est à l'origine de nombreux avantages en termes de performance, surtout si le cache ou la mémoire RAM a un gros temps d'accès. Mais elle est surtout pertinente sur des processeurs particuliers, que nous verrons à la fin du cours. Pour les connaisseurs, c'est très utile si le processeur a un pipeline, ou d'autres techniques du genre comme l’exécution dans le désordre. Pour les processeurs simples, cette technique a un intérêt limité.

Le préchargement des instructionsModifier

La prefetch input queue permet au processeur de charger des instructions à l'avance, si les conditions adéquates sont réunies. À chaque cycle d'horloge, l'étape de chargement charge une nouvelle instruction dans cette file d'instruction. Rappelons qu'une instruction peut prendre plusieurs cycles d'horloge pour s’exécuter, ce qui fait que des instructions sont donc préchargées à l'avance. Ce faisant, l'étape de chargement et de décodage n'ont pas à être synchronisées parfaitement. Les instructions y sont conservées dans leur ordre d'arrivée, afin qu'elles soient exécutées dans le bon ordre. La file d'instruction est donc une mémoire de type FIFO. L'étape de décodage pioche l'instruction à décoder dans la file d'instruction, cette instruction étant par définition la plus ancienne, puis la retire de la file. Le premier processeur commercial grand public à utiliser cette méthode était le 8086, un processeur d'Intel de jeu d'instruction x86. Il pouvait précharger à l'avance 6 octets d’instruction.

Le premier avantage de cette méthode est que l'étape de décodage peut prendre son temps pour décoder des instructions complexes, sans pour autant mettre en pause l'étape de chargement. L'utilité de la file d’instruction prend tout son sens si une instruction met plusieurs cycles d'horloge à être décodée. C'est le cas pour les instructions très complexes, notamment sur les décodeurs micro-codés. Sans file d'instruction, le décodage de ces instructions prendrait plusieurs cycles et stopperait l'étape de chargement. Un autre exemple est quand le processeur exécute une instruction très longue, qui prend plusieurs cycles d'horloge, ce qui bloque l’exécution de nouvelles opérations et potentiellement leur décodage. Mais avec la file d’instruction, les instructions suivantes sont chargées en avance et s'accumulent dans la file d'instruction. Le processeur n'a ainsi pas besoin d'attendre la mémoire pour charger une nouvelle instruction : tout a été chargé à l'avance.

Un autre avantage est que le processeur n'a pas à attendre la mémoire pour commencer le décodage des instructions, ce qui est un gros avantage si la mémoire est lente. Par exemple, prenons l'exemple d'un processeur qui exécute une instruction à chaque cycle d’horloge, chaque instruction étant codée sur 2 octets. On suppose que la mémoire est capable de charger 8 octets à chaque cycle. La mémoire doit garantir un débit constant de 2 octets par cycle, ce qui donne une belle marge. Le problème est que la mémoire peut être utilisée pour des accès mémoire concurrents, simultanés avec la lecture de l'instruction, pour lire des données ou autre. Si le processeur précharge 8 octets d'un coup, il peut lire les 4 instructions depuis la file d'instruction, sans accéder à la mémoire. La même chose est impossible avec un simple registre d'instruction, car il ne peut conserver qu'une instruction, pas plusieurs.

Une autre situation est celle où la mémoire a un temps d'accès important. Par exemple, supposons que le processeur puisse charger une instruction de 2 octets par cycle, mais que la mémoire puisse charger 8 octets en deux cycles. Les temps d'accès semblent incompatibles : 1 cycle pour le processeur, 2 pour la mémoire. Mais la différence de débit peut être utilisée pour précharger des instructions. Le processeur peut charger plus de 2 instructions à la fois, en un seul accès mémoire, puis les exécute alors l'une après l'autre, une par cycle d'horloge. Et quand la dernière instruction préchargée est exécutée, la mémoire est de nouveau libre, ce qui masque la latence des accès mémoire. L'exemple en question est certes un peu tordu, mais les processeurs modernes ne sont pas à l'abri de ce genre de problèmes. Rappelons que le temps d'accès d'un cache est assez long, surtout pour les niveaux de cache inférieurs.

La même méthode peut s'appliquer, mais entre le décodeur et le chemin de données. Il est possible de mettre une file d'attente entre le séquenceur et le chemin de donnée, qui contient des micro-opérations, des instructions décodées. Les micro-opérations peuvent s'accumuler dans cette mémoire, au cas où. Là encore, c'est très utile si l’exécution d'une instruction prend trop de temps. Au lieu de mettre en attente le séquenceur, celui-ci peut prendre de l'avance et charger/décoder les instructions suivantes à l'avance, pendant qu'une instruction multicycles s’exécute. Les instructions chargées à l'avance sont disponibles immédiatement, une fois que le processeur a fini de décoder l'instruction complexe. Cela évite de démarrer le chargement/décodage une fois que l'instruction est terminée, ce qui gâche un peu de temps. La file d'attente entre séquenceur/décodeur et chemin de données est appelé la fenêtre d'instruction, ou encore l'instruction window en anglais.

Les problèmes liés à la Prefetch input queueModifier

Notons que cette méthode permet de charger à l'avance des instructions dont on ne sait pas si elles seront exécutées et que cela peut poser problème. Le problème en question tient dans les branchements. La file d'attente charge à l'avance plusieurs instructions. Mais si un branchement est chargé, toutes les instructions chargées après sont potentiellement invalides. Si le branchement est non-pris, les instructions chargées sont valides, elles sont censées s’exécuter. Mais si le branchement est pris, elles ont été chargées à tort et ne doivent pas s’exécuter. Pour éviter cela, la file d'attente est vidée quand le processeur détecte un branchement. Cette détection ne peut se faire qu'une fois le branchement décodé : on ne sait pas si l'instruction est un branchement ou autre chose tant que le décodage n'a pas eu lieu, par définition. En clair, le décodeur invalide la file d'attente et la vide quand il détecte un branchement.

Les interruptions posent le même genre de problèmes. Il faut impérativement vider la Prefetch input queue quand une interruption survient, avant de la traiter.

Un autre défaut est que la Prefetch input queue se marie assez mal avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. De nos jours, ces techniques peuvent être utilisées très rarement pour compresser un programme et/ou le rendre indétectable (très utile pour les virus informatiques). Le problème avec la Prefetch input queue survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans la file d'attente sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas est quelque peu complexe. Il faut en effet vider la Prefetch input queue si le cas arrive, ce qui demande d'identifier les écritures qui écrasent des instructions préchargées. C'est parfaitement possible, mais demande de transformer la Prefetch input queue en une mémoire hybride, à la fois mémoire associative et mémoire FIFO. Cela ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place, le code automodifiant est alors buggé.

La macro-fusion et la micro-fusionModifier

La Prefetch input queue permet d'ajouter des optimisations au processeur, qui ne seraient pas possibles sans elle. L'une d'entre elle est la macro-fusion, une technique qui permet de fusionner une suite d'instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (multiply and add), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Et enfin, il est possible de fusionner une instruction de test et une instruction de branchement en une seule micro-opération de comparaison-branchement.

La macro-fusion est faisable avant le décodage, quand les instructions sont encore dans la Prefetch input queue. La fusion a lieu au moment du chargement d'une nouvelle instruction. Aussi, on ne devrait pas dire que les instructions machines sont fusionnées en micro-opération, ces dernières n'apparaissant qu'après l'étape de décodage. Mais l'idée est que deux instructions machines sont fusionnées en une seule, qui est décodée en une seule instruction machine. L'avantage de cette technique est que les décodeurs et le chemin de données sont utilisés plus efficacement.