Fonctionnement d'un ordinateur/L'unité de chargement et le program counter
L'unité de contrôle s'occupe du chargement des instructions et de leur interprétation, leur décodage. Elle contient deux circuits : l'unité de chargement qui charge l'instruction depuis la mémoire, et le séquenceur qui commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux, notamment pour gérer les branchements. Dans ce chapitre, nous allons nous intéresser au chargement d'une instruction depuis la RAM/ROM, nous verrons l'étape de décodage de l'instruction au prochain chapitre.
L'unité de chargement a pour cœur le program counter, le registre qui mémorise l'adresse de l'instruction à charger. Il s'agit précisément d'un compteur, vu que le contenu du registre est incrémenté à chaque fois qu'une instruction est chargée. Le program counter est remis à zéro lors du démarrage du processeur, mais est aussi modifié par les instructions de branchement qui écrivent une adresse dedans.
Son interface est la suivante : une entrée d'horloge, une entrée de Reset, une entrée Enable qui indique quand l'incrémenter (renommée Instruction Fetch), et deux entrées pour les branchements. Pour les branchements, il y a une entrée pour l'adresse de destination, une autre pour autoriser l'écriture de celle-ci dans le program counter. La sortie du program counter est reliée au bus d'adresse mémoire, plus ou moins directement.

Les interconnexions entre séquenceur et bus mémoire
[modifier | modifier le wikicode]Pour lire une instruction, le processeur envoie le program counter sur le bus d'adresse et récupère l'instruction sur le bus de données. L'instruction lue est alors envoyée au séquenceur. Et cela demande de connecter le séquenceur au bus mémoire, à la fois sur le bus d'adresse et le bus de données. Et sur ce point, la situation est différente selon que l'on parler d'une architecture Harvard ou Von Neumann.
Les architectures Harvard : des bus mémoire séparés pour la RAM et la ROM
[modifier | modifier le wikicode]Sur les architectures Harvard, le séquenceur et le chemin de donnée utilisent des interfaces mémoire séparées. Le séquenceur est directement relié au bus mémoire de la ROM, alors que le chemin de données est connecté à la RAM.

Le program counter est envoyé sur le bus d'adresse de la ROM, l'instruction est récupérée sur le bus de données de la ROM. Pour la mémoire RAM, elle échange des données avec le chemin de données, notamment avec les registres généraux. Les adresses utilisées pour la RAM viennent elles soit du chemin de données, soit de l'unité de contrôle, tout dépend du mode d'adressage. Mais le program counter n'est pas impliqué.

Les architectures Harvard modifiées doivent cependant rajouter une connexion entre le bus ROM et les registres généraux. C'est nécessaire pour charger une donnée constante depuis la mémoire ROM. Rappelons que la donnée constante est copiée dans un registre général, donc dans le chemin de données.

Les architectures Von-Neumann : un bus mémoire unique
[modifier | modifier le wikicode]Sur les architectures Von-Neumann et affiliées, le séquenceur et le chemin de donnée partagent la même interface mémoire. Et cela pose deux problèmes.
Le premier problème est que le bus mémoire doit être libéré une fois l'instruction chargée, pour un éventuel accès mémoire. Et le séquenceur doit conserver une copie de l'instruction chargée, sans quoi il ne peut pas décoder l'instruction correctement. Par exemple, si l'instruction met plusieurs cycles à s'exécuter, le séquenceur doit conserver une copie de l'instruction durant ces plusieurs cycles. Pour ça, le processeur intègre un registre d'instruction situé juste avant le séquenceur, qui mémorise l'instruction chargée.

Le second problème est de gérer le flot des instructions/données entre le bus mémoire, le chemin de données et le séquenceur. Si le processeur lit une instruction, le bus doit être relié à l'unité de contrôle. Par contre, s'il accède à une donnée, il doit être relié au chemin de données. Le processeur doit connecter l'interface mémoire soit au séquenceur, soit au chemin de données et cela complique le réseau d'interconnexion interne au processeur.

Une première solution utilise un bus unique qui relie l'interface mémoire, le séquenceur et le chemin de données. Pour charger une instruction, le séquenceur copie le program counter sur le bus d'adresse, attend que l'instruction lue soit disponible sur le bus de données, puis la copie dans le registre d'instruction. Le bus mémoire est alors libre et peut être utilisé pour lire ou écrire des données, si le besoin s'en fait sentir.
- Il faut noter que cette solution implique d'utiliser des registres d’interfaçage avec la mémoire.

Une autre solution utilise deux bus interne séparés : un connecté au bus d'adresse, l'autre au bus de données. Le program counter est alors connecté au bus interne d'adresse, le séquenceur est relié au bus interne de données. Notons que la technique marche bien si le program counter est dans le banc de registre : les interconnexions utilisées pour gérer l'adressage indirect permettent d'envoyer le program counter sur le bus d'adresse sans ajout de circuit.
Le tout peut être amélioré en remplaçant les deux bus par des multiplexeurs et démultiplexeurs. Le bus d'adresse est précédé par un multiplexeur, qui permet de faire le choix entre Program Counter, adresse venant du chemin de données, ou adresse provenant du séquenceur (adressage absolu). De même, le bus de données est suivi par un démultiplexeur qui envoie la donnée/instruction lue soit au registre d'instruction, soit au chemin de données. Le tout se marie très bien avec les chemins de donnée vu dans le chapitre précédent. Au passage, il faut noter que cette solution nécessite un banc de registre multi-port.

Le chargement d'une instruction
[modifier | modifier le wikicode]
L'étape de chargement (ou fetch) doit faire deux choses : mettre à jour le program counter et lire l'instruction en mémoire RAM ou en ROM. Les deux étapes peuvent être faites en parallèle, dans des circuits séparés. Pendant que l'instruction est lue depuis la mémoire RAM/ROM, le program counter est incrémenté et/ou altéré par un branchement. Pour lire l'instruction, on envoie le program counter sur le bus d'adresse et on récupère l'instruction sur le bus de données pour l'envoyer sur l'entrée du séquenceur.
Le chargement des instructions de longueur variable
[modifier | modifier le wikicode]Le chargement des instructions de longueur variable pose de nombreux problèmes. Le premier 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 solution la plus simple indique la longueur de l'instruction dans une partie de l'opcode ou de la représentation en binaire de l'instruction. Mais les processeurs haute performance de nos PC utilisent d'autres solutions, qui n'encodent pas explicitement la taille de l'instruction.
Les processeurs récents utilisent un registre d'instruction de grande taille, capable de mémoriser l'instruction la plus longue du processeur. Par exemple, si un processeur supporte des instructions allant de 1 à 8 octets, comme les CPU x86, le registre d'instruction fera 8 octets. Le décodeur d'instruction vérifie à chaque cycle le contenu du registre d'instruction. Si une instruction est détectée dedans, son décodage commence, peu importe la taille de l'instruction. Une fois l'instruction exécutée, elle est retirée du registre d'instruction. Reste à alimenter le registre d'instruction, à charger des instructions dedans. Et pour cela deux solutions : soit on charge l'instruction en plusieurs fois, soit d'un seul coup.
La solution la plus simple charge les instructions par mots de 8, 16, 32, 64 bits. Ils sont accumulés dans le registre d'instruction jusqu'à détecter une instruction complète. La technique marche nettement mieux si les instructions sont très longues. Par exemple, les CPU x86 ont des instructions qui vont de 1 à 15 octets, ce qui fait qu'ils utilisent cette technique. Précisément, elle marche si les instructions les plus longues sont plus grandes que le bus mémoire.
Une autre solution charge un bloc de mots mémoire aussi grand que la plus grande instruction. Par exemple, si le processeur gère des instructions allant de 1 à 4 octets, il charge 4 octets d'un seul coup dans le registre d'instruction. Il va de soit que cette solution ne marche que si les instructions du processeur sont assez courtes, au plus aussi courtes que le bus mémoire. Ainsi, on est sûr de charger obligatoirement au moins une instruction complète, et peut-être même plusieurs. Le contenu du registre d'instruction est alors découpé en instructions au fil de l'eau.
Utiliser un registre d'instruction large 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. Le problème se manifeste peu importe que l'instruction soit chargée d'un seul coup ou bien en plusieurs fois.

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.

Une autre solution se passe de registre d'instruction en utilisant à la place l'état interne du séquenceur. Là encore, elle charge l'instruction morceau par morceau, typiquement par blocs de 8, 16 ou 32 bits. Les morceaux ne sont pas accumulés dans le registre d'instruction, la solution utilise à la place l'état interne du séquenceur. 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 à un autre, chaque état interne encodant les mots mémoire chargés auparavant. S'il tombe sur le dernier mot mémoire d'une instruction, il rentre dans un état de décodage.
Le chargement avec les jeux d'instruction compacts
[modifier | modifier le wikicode]Pour finir, il faut parler du cas particulier des jeux d'instruction compacts. Pour rappel, ces jeux d'instruction sont présents sur certains CPU RISC 32 bits. De tels processeurs gèrent un jeu d'instruction 32 bits normal, qui encode ses instructions sur 32 bits, et un jeu d'instruction plus compact qui encode ses instructions sur 16 bits. Nous allons prendre l'exemple du jeu d'instruction emblématique de cette catégorie : l'ARM thumb, qui est le jeu d'instruction compact associé au jeu d'instruction ARM 32 bits.
Les deux jeux d'instruction ont un mode d'exécution chacun, ce qui veut dire que le processeur est configuré pour fonctionner en mode thumb ou en mode ARM 32 bits. En clair, on ne peut pas mélanger les deux jeu d'instructions en même temps. La raison est qu'une même suite de bit correspondait soit à une instruction ARM normale, soit à deux instructions thumbs. Pour faire la différence, il faut préciser dans quel mode d'exécution le CPU est : mode ARM ou mode thumb. Le processeur interpréte les instructions correctement en tenant compte de ce mode.
Intuitivement, on se dirait qu'il y a deux décodeurs séparés : un utilisé en mode ARM, un autre utilisé en mode thumb. Sauf que ce n'est pas la solution retenue. A la place, les ingénieurs d'ARM profitent d'une propriété du thumb : thumb est un sous-ensemble du jeu d’instruction 32 bits. Par exemple, une instruction thumb a un équivalent en RAM 32 bits, bien que l'encodage entre les deux est différent. Et cette propriété est utilisée pour faciliter l'implémentation au niveau du décodeur. Le processeur contient simplement un circuit qui convertit les instructions thumb en leur équivalent ARM 32 bits. Le circuit de conversion se situe en aval de l'unité de chargement, juste avant le décodeur d'instruction. Le circuit est désactivé quand le processeur fonctionne en mode ARM, mais est active en mode thumb.
Les optimisations du code automodifiant
[modifier | modifier le wikicode]Précédemment dans ce cours, nous avons parlé du code automodifiant, à savoir le fait qu'un programme modifie ses propres instructions à la volée. Il était utilisé pour avoir des tableaux, sur des processeurs qui ne supportaient que le mode d'adressage direct. L'idée était que les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination, incorporée dans l'instruction via adressage absolu, était changée via code automodifiant.
Quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant. Elles agissaient toutes sur le registre d'instruction, en permettant de modifier son contenu. Les instructions étaient chargées dans le registre d'instruction et étaient modifiées dans ce registre, avant d'être décodées. La modification était généralement assez simple : appliquer un masque, additionner une constante, guère plus. Les processeurs de ce type sont très rares, j'en connais deux exemples : le Burroughs B1700 et l'Apollo Guidance Computer.
Le Burroughs B1700 permettait d'appliquer un masque sur l'instruction lue. Pour cela, il permettait de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre d'instruction. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié.
Un autre exemple est celui de l'Apollo Guidance Computer, qui permettait d'altérer le registre d'instruction avec une instruction dédiée, l'instruction Index next instruction, que nous appellerons INI. Nous avons déjà vu cette instruction INI, dans le chapitre sur les modes d'adressage, mais nous pouvons maintenant expliquer comment elle est implémentée. Elle additionne une constante à l'instruction suivante. Elle était utilisée pour ajouter un décalage à une adresse, dans une instruction LOAD/STORE, ou un branchement direct. L'addition était vraisemblablement réalisée par l'ALU du processeur. La conséquence est que le registre d'instruction était un registre architectural, qui était adressé implicitement, avec un mode d'adressage implicite.
Un dernier exemple, plus subtil, est celui du coprocesseur d'entrées-sortie PIO présent sur les Rasberry Pi. Son instruction MOV peut copier une donnée d'une source vers une destination, source et destination étant très variées. Elle pouvait écrire dans les registres, mais aussi dans le Program Counter et le registre d'instruction. Écrire dans le Program Counter permettait de faire des branchements inconditionnels, écrire dans le registre d'instruction permettait d'exécuter le contenu d'un registre comme si c'était une instruction. Cela permet d'ajouter des instructions à un programme, sans modifier le programme en mémoire RAM. La possibilité n'est sans doute pas beaucoup utilisée, mais elle existe !
L'incrémentation du program counter
[modifier | modifier le wikicode]À chaque chargement, le program counter est mis à jour afin de pointer sur la prochaine instruction à charger. Sur la quasi-totalité des processeurs, les instructions sont placées dans l'ordre d’exécution dans la RAM. En faisant ainsi, on peut mettre à jour le program counter en lui ajoutant la longueur de l'instruction courante. Déterminer la longueur de l'instruction 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. Dans ce qui va suivre, nous allons supposer que les instructions sont de taille fixe, ce qui fait que le program counter est toujours incrémenté de la même valeur.
Il existe deux méthodes principales pour incrémenter le program counter : 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. En tout, cela donne quatre organisations possibles.
- Un program counter séparé relié à un incrémenteur séparé.
- Un program counter séparé des autres registres, incrémenté par l'ALU.
- Un program counter intégré au banc de registre, relié à un incrémenteur séparé.
- Un program counter intégré au banc de registre, incrémenté par l'ALU.

Les processeurs haute performance modernes utilisent tous la première méthode. Les autres méthodes étaient surtout utilisées sur les processeurs 8 ou 16 bits, elles sont encore utilisées sur quelques microcontrôleurs de faible puissance. Nous auront l'occasion de donner des exemples quand nous parlerons des processeurs 8 bits anciens, dans le chapitre sur les architectures à accumulateur et affiliées.
Le program counter mis à jour par l'ALU
[modifier | modifier le wikicode]Sur certains processeurs, le calcul de l'adresse de la prochaine instruction est effectué par l'ALU. L'avantage de cette méthode est qu'elle économise un incrémenteur dédié au program counter. Ce qui explique que cette méthode était utilisée sur les processeurs assez anciens, à une époque où un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Le désavantage principal est une augmentation de la complexité du séquenceur, qui doit gérer la mise à jour du program counter. De plus, 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.
Si on incrémente le program counter avec l'ALU, il est intéressant de le placer dans le banc de registres. Un désavantage est qu'on perd un registre. Par exemple, avec un banc de registre de 16 registres, on ne peut adresser que 15 registres généraux. De plus ce n'est pas l'idéal pour le décodage des instructions et pour le séquenceur. Si le processeur dispose de plusieurs bancs de registres, le program counter est généralement placé dans le banc de registre dédié aux adresses. Sinon, il est placé avec les nombres entiers/adresses.
D'anciens processeurs incrémentaient le program counter avec l'ALU, mais utilisaient bien un registre séparé pour 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’exécution d'une instruction, il est connecté au bus interne pour l'incrémenter. C'est le séquenceur qui gère le tout. 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.
Le compteur programme
[modifier | modifier le wikicode]Dans la quasi-totalité des processeurs modernes, 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. Dans ce qui suit, nous allons appeler ce registre compteur : le compteur ordinal. L'usage d'un compteur simplifie fortement la conception du séquenceur. Le séquenceur n'a pas à gérer la mise à jour du program counter, sauf en cas de branchements. De plus, il est possible d'incrémenter le program counter 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 un incrémenteur dédié au program counter.
Sur les processeurs très anciens, les instructions faisaient toutes un seul mot mémoire et le compteur ordinal était un circuit incrémenteur des plus basique. Sur les processeurs modernes, le compteur est incrémenté par pas de 4 si les instructions sont codées sur 4 octets, 8 si les instructions sont codées sur 8 octets, etc. Concrètement, le compteur est un circuit incrémenteur auquel on aurait retiré quelques bits de poids faible. Par exemple, si les instructions sont codées sur 4 octets, on coupe les 2 derniers bits du compteur. Si les instructions sont codées sur 8 octets, on coupe les 3 derniers bits du compteur. Et ainsi de suite.
Une amélioration de la solution précédente utilise un circuit incrémenteur partagé entre le program counter et d'autres registres. L'incrémenteur est utilisé pour incrémenter le program counter, mais aussi pour effectuer d'autres calculs d'adresse. Par exemple, sur les architectures avec une pile d'adresse de retour, il est possible de partager l'incrémenteur/décrémenteur avec le pointeur de pile ( la technique ne marche pas avec une pile d'appel). Un autre exemple est celui des processeurs qui gèrent automatiquement le rafraichissement mémoire, grâce à, un compteur intégré dans le processeur. Il est possible de partager l'incrémenteur du program counter avec le compteur de rafraichissement mémoire, qui mémorise la prochaine adresse mémoire à rafraichir. Nous avions déjà abordé ce genre de partage dans le chapitre sur le chemin de données, dans l'annexe sur le pointeur de pile, mais nous verrons d'autres exemples dans le chapitre sur les architectures hybrides accumulateur-registre 8/16 bits.
Il a existé quelques processeurs pour lesquelles le program counter était non pas un compteur binaire classique, mais un linear feedback shift register. L'avantage est que le circuit d'incrémentation utilisait bien moins de portes logiques, l'économie était substantielle. Mais un défaut est que des instructions consécutives dans le programme n'étaient pas consécutives en mémoire. Il existait cependant une table de correspondance pour dire : la première instruction est à telle adresse, la seconde à telle autre, etc. Un exemple de processeur de ce type est le TMS 1000 de Texas Instrument, un des premiers microcontrôleur 4 bits datant des années 70.
Quand est mis à jour le program counter ?
[modifier | modifier le wikicode]Le program counter est mis à jour quand il faut charger une nouvelle instruction. Reste qu'il faut savoir quand le mettre à jour. Incrémenter le program counter à intervalle régulier, par exemple à chaque cycle d’horloge, fonctionne sur les processeurs où toutes les instructions mettent le même temps pour s’exécuter. Mais de tels processeurs sont très rares. Sur la quasi-totalité des processeurs, les instructions ont une durée d’exécution variable, elles ne prennent pas le même nombre de cycle d'horloge pour s’exécuter. Le temps d’exécution d'une instruction varie selon l'instruction, certaines sont plus longues que d'autres. Tout cela amène un problème : comment incrémenter le program counter avec des instructions avec des temps d’exécution variables ?
La réponse est que la mise à jour du program counter démarre quand l'instruction précédente a terminée de s’exécuter, plus précisément un cycle avant. C'est un cycle avant pour que l'instruction chargée soit disponible au prochain cycle pour le décodeur. Pour cela, le séquenceur doit déterminer quand l'instruction est terminée et prévenir le program counter au bon moment. Il faut donc une interaction entre séquenceur et circuit de chargement. Le circuit de chargement contient une entrée Enable, qui autorise la mise à jour du program counter. Le séquenceur met à 1 cette entrée pour prévenir au program counter qu'il doit être incrémenté au cycle suivant ou lors du cycle courant. Cela permet de gérer simplement le cas des instructions multicycles.

Toute la difficulté est alors reportée dans le séquenceur, qui doit déterminer la durée d'une instruction. Pour gérer la mise à jour du program counter, le séquenceur doit déterminer la durée d'une instruction. Cela est simple pour les instructions arithmétiques et logiques, qui ont un nombre de cycles fixe, toujours le même. Une fois l'instruction identifiée par le séquenceur, il connait son nombre de cycles et peut programmer un compteur interne au séquenceur, qui compte le nombre de cycles de l'instruction, et fournit le signal Enable au program counter. Mais cette technique ne marche pas vraiment pour les accès mémoire, dont la durée n'est pas connue à l'avance, surtout sur les processeurs avec des mémoires caches. Pour cela, le séquenceur détermine quand l'instruction mémoire est finie et prévient le program counter quand c'est le cas.
Il existe quelques processeurs pour lesquels le temps de calcul dépend des opérandes de l'instruction. On peut par exemple avoir une division qui prend 10 cycles avec certaines opérandes, mais 40 cycles avec d'autres opérandes. Mais ces processeurs sont rares et cela est surtout valable pour les opérations de multiplication/division, guère plus. Le problème est alors le même qu'avec les accès mémoire et la solution est la même : l'ALU prévient le séquenceur quand le résultat est disponible.
Les branchements et le program counter
[modifier | modifier le wikicode]Un branchement consiste juste à écrire l'adresse de destination dans le program counter. L'implémentation des branchements demande d'extraire l'adresse de destination et d'altérer le program counter quand un branchement est détecté. Pour cela, le décodeur d'instruction détecte si l'instruction exécutée est un branchement ou non, et il en déduit où trouver l'adresse de destination.
L'adresse de destination se trouve à un endroit qui dépend du mode d'adressage du branchement. Pour rappel, on distingue quatre modes d'adressage pour les branchements : direct, indirect, relatif et implicite. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le séquenceur. Pour les branchements indirects, l'adresse de destination est dans le banc de registre. Pour les branchements relatifs, il faut ajouter un décalage au program counter, décalage extrait de l'instruction par le séquenceur. 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.
L'altération du program counter dépend de si le program counter est dans un compteur séparé ou s'il est dans le banc de registre. Nous avons vu plus haut qu'il y a quatre cas différents, mais nous n'allons voir que deux cas : celui où le program counter est dans le banc de registre et est incrémenté par l'unité de calcul, celui avec un program counter séparé avec son propre incrémenteur. Les deux autres cas ne sont utilisés que sur des architectures à accumulateur ou à pile spécifiques, que nous n'avons pas encore vu à ce stade du cours et que nous verrons en temps voulu dans le chapitre sur ces architectures.
L’implémentation des branchements avec un program counter intégré au banc de registres
[modifier | modifier le wikicode]Le cas le plus simple est celui où le program counter est intégré au banc de registre, avec une incrémentation faite par l'unité de calcul. Charger une instruction revient alors à effectuer une instruction LOAD en adressage indirect, à deux différences près : le registre sélectionné est le program counter, l’instruction est copiée dans le registre d'instruction et non dans le banc de registre. L'incrémentation du porgram counter est implémenté avec des micro-opérations qui agissent sur le chemin de données, au même titre que les branchements indirects, relatifs et directs.
- Les branchements indirects copient un registre dans le program counter, ce qui revient simplement à faire une opération MOV entre deux registres, dont le program counter est la destination.
- L'opération inverse d'un branchement indirect, qui copie le program counter dans un registre général, est utile pour sauvegarder l'adresse de retour d'une fonction.
- Les branchements directs copient une adresse dans le program counter, ce qui les rend équivalents à une opération MOV avec une constante immédiate, dont le program counter est la destination.
- Les branchements relatifs sont une opération arithmétique entre le program counter et un opérande en adressage immédiat.
En clair, à partir du moment où le chemin de données supporte les instructions MOV et l'addition, avec les modes d'adressage adéquat, il supporte les branchements. Aucune modification du chemin de données n'est nécessaire, le séquenceur gére le chargement d'une instruction avec les micro-instructions adéquates : une micro-opération ADD pour incrémenter le program counter, une micro-opération LOAD pour le chargement de l'instruction.
Notons que sur certaines architectures, le program counter est adressable au même titre que les autres registres du banc de registre. Les instructions de branchement sont alors remplacées par des instructions MOV ou des instructions arithmétique équivalentes, comme décrit plus haut.
L’implémentation des branchements avec un program counter séparé
[modifier | modifier le wikicode]Étudions maintenant 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ée 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 :

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

Les branchements relatifs sont ceux qui font sauter X instructions plus loin dans le programme. Leur implémentation demande d'ajouter une constante au program counter, la constante étant fournie dans l’instruction. Là encore, deux solutions sont possibles : réutiliser l'ALU pour calculer l'adresse, ou utiliser un additionneur séparé. L'additionneur séparé peut être fusionné avec l'additionneur qui incrémente le program counter pour passer à l’instruction suivante.

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.

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.
Les optimisations du chargement des instructions
[modifier | modifier le wikicode]Charger une instruction est techniquement une forme d'accès mémoire un peu particulier. En clair, charger une instruction prend au minimum un cycle d'horloge, et cela peut rapidement monter à 3-5 cycles, si ce n'est plus. L'exécution des instructions est alors fortement ralentit par la mémoire. Par exemple, imaginez que la mémoire mette 3 cycles d'horloges pour charger une instruction, alors qu'une instruction s’exécute en 1 cycle (si les opérandes sont dans les registres). La perte de performance liée au chargement des instructions est alors substantielle. Heureusement, il est possible de limiter la casse en utilisant des mémoires caches, ainsi que d'autres optimisations, que nous allons voir dans ce qui suit.
Le tampon de préchargement
[modifier | modifier le wikicode]La technique dite du préchargement est utilisée dans le cas où la mémoire a un temps d'accès important. Mais si la latence de la RAM est un problème, le débit ne l'est pas. Il est possible d'avoir une RAM lente, mais à fort débit. Par exemple, supposons que la mémoire puisse charger 4 instructions (de taille fixe) en 3 cycles. Le processeur peut alors charger 4 instructions en un seul accès mémoire, et les exécuter l'une après l'autre, une par cycle d'horloge. Les temps d'attente sont éliminés : le processeur peut décoder une nouvelle instruction à chaque cycle. 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.

La seule contrainte est de mettre les instructions préchargées en attente. La solution pour cela est d'utiliser un registre d'instruction très large, capable de mémoriser plusieurs instructions à la fois. Ce registre est de plus connecté à un multiplexeur qui permet de sélectionner l'instruction adéquate dans ce registre. Ce multiplexeur est commandé par le program counter et quelques circuits annexes. Ce super-registre d'instruction est appelé un tampon de préchargement.
La méthode a une implémentation nettement plus simple avec des instructions de taille fixe, alignées en mémoire. La commande du multiplexeur de sélection de l'instruction est alors beaucoup plus simple : il suffit d'utiliser les bits de poids fort du program counter. Par exemple, prenons le cas d'un registre d'instruction de 32 octets pour des instructions de 4 octets, soit 8 instructions. Le choix de l'instruction à sélectionner se fait en utilisant les 3 bits de poids faible du program counter.
Mais cette méthode ajoute de nouvelles contraintes d'alignement, similaires à celles vues dans le chapitre sur l'alignement et le boutisme, sauf que l'alignement porte ici sur des blocs d'instructions de même taille que le tampon de préchargement. Si on prend l'exemple d'un tampon de préchargement de 128 bits, les instructions devront être alignées par blocs de 128 bits. C'est à dire qu'idéalement, les fonctions et autres blocs de code isolés doivent commencer à des adresses multiples de 128, pour pouvoir charger un bloc d'instruction en une seule fois. Sans cela, les performances seront sous-optimales.
- Il arrive que le tampon de préchargement ait la même taille qu'une ligne de cache.
Lorsque l'on passe d'un bloc d'instruction à un autre, le tampon de préchargement est mis à jour. Par exemple, si on prend un tampon de préchargement de 4 instructions, on doit changer son contenu toutes les 4 instructions. La seule exception est l'exécution d'un branchement. En effet, lors d'un branchement, la destination du branchement n'est pas dans le tampon de préchargement et elle doit être chargée dedans (sauf si le branchement pointe vers une instruction très proche, ce qui est improbable). Pour cela, le tampon de préchargement est mis à jour précocement quand le processeur détecte un branchement.
Le même problème survient 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. Le problème survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans le tampon de préchargement sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas demande de vider le tampon de préchargement si le cas arrive, mais ça 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é.
Le Zero-overhead looping
[modifier | modifier le wikicode]Nous avions vu dans le chapitre sur les instructions machines, qu'il existe des instructions qui permettent de grandement faciliter l'implémentation des boucles. Il s'agit de l'instruction REPEAT, utilisée sur certains processeurs de traitement de signal. Elle répète l'instruction suivante, voire une série d'instructions, un certain nombre de fois. Le nombre de répétitions est mémorisé dans un registre décrémenté à chaque itération de la boucle. Elle permet donc d'implémenter une boucle FOR facilement, sans recourir à des branchements ou des conditions.
Une technique en lien avec cette instruction permet de grandement améliorer les performances et la consommation d'énergie. L'idée est de mémoriser la boucle, les instructions à répéter, dans une petite mémoire cache située entre l'unité de chargement et le séquenceur. Le cache en question s'appelle un tampon de boucle, ou encore un hardware loop buffer. Grâce à lui, il n'y a pas besoin de charger les instructions à chaque fois qu'on les exécute, on les charge une bonne fois pour toute : c'est plus rapide. Bien sûr, il ne fonctionne que pour de petites boucles, mais il en est de même avec l'instruction REPEAT.