Fonctionnement d'un ordinateur/L'unité de chargement et le program counter

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, 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 pour gérer les branchements, pour charger les instructions au bon moment, etc. L'unité de chargement contient le program counter et les circuits associés, ainsi que des circuits pour communiquer avec la mémoire. Les circuits connectés à la mémoire permettent non seulement d'envoyer le program counter sur le bus d'adresse, mais aussi de récupérer l'instruction chargée sur le bus de données.

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.

Le chargement d'une instruction demande d'effectuer trois étapes assez simples. Mais malgré leur simplicité, il y a beaucoup à dire dessus. La mise à jour du program counter est un peu à part vu qu'il s'agit d'un sujet assez velu, mais son envoi sur le bus d'adresse et la lecture de l'instruction demandent eux aussi d'ajouter des circuits au processeur. Voyons-les en détail.

L'initialisation du program counter

modifier

Lors du démarrage ou redémarrage, le processeur est conçu pour mettre tous ses registres à 0, sauf éventuellement le program counter. Une fois le processeur démarré, il faut initialiser le program counter 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 toutes, 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.

L'incrémentation du program counter

modifier

À chaque chargement, le program counter est mis à jour afin de pointer sur la prochaine instruction à charger. 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. 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.

Les circuits de mise à jour du program counter

modifier

Une fois initialisé, le program counter est incrémenté à chaque fois que le processeur exécute une instruction, sauf en cas de branchements. Il existe trois méthodes principales pour incrémenter le program counter, 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.

Le program counter mis à jour par l'ALU

modifier

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. Ce qui explique que cette méthode était utilisée sur les processeurs assez anciens, à une époque où la technologie des semi-conducteurs ne permettait pas de mettre beaucoup de transistors sur une puce électronique. L'économie de circuit était primordiale et un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Autant dire que les processeurs de l'époque gagnaient à utiliser cette technique.

Le désavantage principal est 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.

Si on met à jour le program counter avec l'ALU, alors il est intéressant de le placer dans le banc de registres. 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. 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, s'il existe. Sinon, il est placé avec les nombres entiers/adresses.

D'autres processeurs anciens utilisent l'ALU pour mettre à jour le program counter, mais disposent bien d'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, 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.

Le compteur programme

modifier

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. Dans ce qui suit, nous allons appeler cette mise en oeuvre sous le nom de compteur ordinal.

Sur les processeurs très anciens, le compteur ordinal était un simple circuit incrémenteur, car ces processeurs arrivaient à caler leurs instructions sur un seul mot mémoire. Sur les processeurs modernes, c'est aussi le cas, sauf que 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. Notons qu'on peut simplifier le compteur en utilisant un simple 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.

L'usage d'un compteur 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.

Le partage de l'incrémenteur lié au program counter

modifier

Le program counter a donc son propre circuit d'incrémentation, sauf sur des architectures simples où c'est l'ALU qui est utilisée. Et certaines architectures ont tenté d'utiliser cet incrémenteur pour faire d'autres choses. L'idée est de le partager pour effectuer d'autres calculs d'adresse. En effet, les calculs d'adresse sont souvent simples et demandent d'incrémenter ou de décrémenter l'adresse en question.

Il est par exemple possible de partager l'incrémenteur/décrémenteur avec le pointeur de pile. C'est là une utilisation intelligente de l'incrémenteur, qui permet d'éliminer une redondance : ils sont tous deux des registres contenant une adresse, ils sont régulièrement connectés au bus d'adresse, ils sont incrémentés ou décrémentés à chaque fois qu'on les modifie. Il s'agit là d'une simplification assez compliquée à mettre en œuvre, mais qui a son intérêt. Cette technique marche bien si les cadres de pile ont la même taille, s'ils sont de taille fixe : on incrémente/décrémente le pointeur de pile d'une taille fixe, toujours identique. Mais si les cadres de pile sont de taille variable, alors cette solution ne marche pas.

Il est possible de partager l'incrémenteur avec le pointeur de pile, les registres de segmentation, voire avec d'autres registres. Nous allons prendre l'exemple du pointeur de pile, mais ce qui va suivre vaut pour tout autre registre d'adresse. Pour résumer, on a trois cas principaux :

  • Le program counter partage l'incrémenteur/décrémenteur avec un ou plusieurs registres d'adresse.
  • Le program counter est intégré à l'interface mémoire et est mis à jour par l'unité de calcul d'adresse.
  • Le program counter a son propre incrémenteur qui n'est pas partagé.

Les deux premiers cas ont l'avantage de partager l'incrémenteur ou l'ALU entière, alors que le troisième ne permet aucune économie de circuits.

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

Le Z80 est un processeur dans ce style, à quelques détails près. Il dispose d'un circuit incrémenteur séparé de l'ALU, qui est utilisé pour mettre à jour le program counter et le pointeur de pile. Mais il est aussi utilisé pour le rafraichissement mémoire, qui demande de balayer la mémoire d'adresse en adresse, en incrémentant un compteur de rafraichissement.

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, l'incrémenteur du program counter a été réutilisé de beaucoup de manières originales.

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.

Quand est mis à jour le program counter ?

modifier

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 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. La mise à jour du program counter démarre donc quand l'instruction précédente a terminée de s’exécuter. Quelques instructions simples peuvent s’exécuter en une seule instruction machine, mais beaucoup ne sont pas dans ce cas. Une conséquence est que les instructions ont une durée d’exécution variable. Tout cela amène un premier 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 donc 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.

 
Commande de la mise à jour du program counter.

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

L'implémentation des branchements implique à la fois l'unité de chargement et le séquenceur. L'implémentation des branchements demande d'identifier les branchements et d'altérer le program counter quand un branchement est détecté. Pour cela, le séquenceur 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'altération du program counter est par contre du fait de l'unité de chargement.

 
Unité de chargement, program counter.

En soi, un branchement consiste juste à écrire l'adresse de destination dans le program counter. L'adresse en question se trouve à un endroit qui dépend du mode d'adressage du branchement. L'unité de branchement doit être modifiée pour pouvoir modifier le contenu du program counter, en ajoutant un simple multiplexeur. La seule difficulté tient trouver l'adresse de destination du branchement.

Pour rappel, il existe plusieurs types de branchements, et l'implémentation de chaque type est légèrement différente. 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 un registre. Pour les branchements relatifs, il faut ajouter un décalage au program counter, décalage fournit 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. 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 registres

modifier

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 un opérande lu 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'ALU

modifier

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

 
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'envoi du program counter sur le bus d'adresse

modifier

Les program counter doit être envoyé sur le bus d'adresse, et cela demande quelques adaptations. Et sur ce point, la situation est différente selon que l'on parler d'une architecture Harvard ou Von Neumann.

Sur les architectures Harvard, où on a un espace d'adressage séparé pour les instructions, l'implémentation est très simple. Le program counter est directement relié au bus mémoire dédié aux instructions, il est le seul à y être relié, il n'y a rien de spécial à faire. Le séquenceur et le chemin de donnée utilisent des interfaces mémoire séparées.

 
Microarchitecture de l'interface mémoire d'une architecture Harvard

Mais sur les architectures Von-Neumann et affiliées, ou tout du moins les architectures non-Havard, les choses sont différentes. Le séquenceur et le chemin de donnée partagent la même interface mémoire. L'interface avec la mémoire s'occupe alors de toutes les adresses, qu'elles viennent du chemin de données ou du séquenceur, et ca comprend aussi le program counter.

 
Microarchitecture de l'interface mémoire d'une architecture von neumann

La connexion du program counter sur le bus d'adresse, architectures Von Neumann

modifier

Le même bus sert donc soit à envoyer des adresses provenant du chemin de données (calculées/précisées via un mode d'adressage), soit envoyer le program counter. La gestion de ce choix se fait différemment selon que le program counter est isolé, isolé mais connecté au bus interne, ou placé dans le banc de registres.

Si le program counter est intégré au banc de registres, il suffit de connecter la sortie du banc de registre au bus d'adresse, ce qui est équivalent à de l'adressage indirect. L'implémentation est la plus simple possible. L'unité de chargement ne fait que commander le banc de registre et l'interface mémoire.

 
Connexion du program counter sur les bus avec PC dans le banc de registres

Si le program counter est isolé, mais connecté au bus interne au processeur, il suffit de connecter le program counter au bus d'adresse et déconnecter le banc de registre.

 
Connexion du program counter sur les bus avec PC isolé

Une solution presque équivalente utilise un MUX placé avant les registres d'interfaçage, qui permet de faire le choix entre sortie de l'AGU/bus interne au CPU et Program Counter.

 
Unité d'accès mémoire avec program counter
Notez que l'on comprend mieux l'intérêt des registres d'interfaçage. Ce registre contient le program counter, mais celui-ci peut être mis à jour en parallèle. Si on incrémente le program counter pendant l'accès mémoire, le registre d'interfaçage maintient l'adresse adéquate pendant tout l'accès.

Le lien avec les autres registres d'adresse, architecture Von Neumann

modifier

Les trois cas précédents montrent ce qu'il se passe suivant la localisation du program counter. Mais il faut aussi parler du cas où le processeur dispose d'une unité de calcul d'adresse séparée, généralement liée à un banc de registres spécialisés pour les adresses. L'interface mémoire peut alors intégrer ou non le program counter. Les trois cas précédents sont alors quelque peu adaptés.

Dans le cas où le program counter est incrémenté par son propre compteur/circuit, rien ne change.

Dans le cas où le program counter est intégré dans un banc de registre spécialisé pour les adresses, il est généralement incrémenté par l'unité de calcul associée. Dans ce cas, il n'y a rien à faire : la sortie de l'unité de calcul et/ou du banc de registre est reliée au bus d'adresse, il suffit d'adresser le program counter, rien de plus. Un exemple de cette organisation est celui du 8086, un des tout premier processeur d'Intel.

 
Calcul d'adresse par ALU dédiée avec PC intégré au banc de registre d'adresse

Le troisième cas, avec un program counter isolé et incrémenté par l'unité de calcul d'adresse est aussi adapté. Dans ce cas, le program counter est envoyé sur une entrée de l'unité de calcul, à travers un multiplexeur. La sortie de l'unité de calcul est évidemment connectée au program counter, pour l'incrémenter.

 
Calcul d'adresse par ALU dédiée avec PC séparé

Les trois techniques précédentes peuvent s'adapter dans le cas où le program counter est regroupé avec le pointeur de pile.

La lecture de l'instruction

modifier

Passons maintenant à la dernière étape : la lecture de l'instruction proprement dit. L'instruction est alors disponible sur le bus de données, et le processeur doit en faire bon usage.

Le registre d'instruction

modifier

Sur certains processeurs, l'instruction chargée est stockée dans un registre d'instruction situé juste avant le séquenceur. Les processeurs basés sur une architecture Harvard peuvent se passer de ce registre, vu que l'instruction reste disponible sur le bus des instructions 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. Et cela demande de mémoriser l'instruction dans un registre pour libérer le bus, d'où l'existence du registre d'instruction.

 
Registre d'instruction.

Le chargement des instructions de longueur variable

modifier

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 a é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. 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 tampon de préchargement

modifier

La technique dite du préchargement est utilisée dans le cas où la mémoire a un temps d'accès important. 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. Le chargement des instructions est alors fortement ralentit par la mémoire, le chargement de chaque instruction prend 3 cycles. 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.

 
Tampon de préchargement d'instruction

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

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.