Fonctionnement d'un ordinateur/Les exceptions précises et branchements
Les dépendances de contrôle sont liés aux branchements, aux exceptions et aux interruptions. En théorie, les instructions qui suivent un branchement ne doivent pas être chargées ni exécutées tant que le branchement n'est pas terminé. Il faut dire qu'il ne sait pas si le branchement est pris ou non, ni quelle adresse charger suite au branchement. Mais le problème est qu'un branchement met plusieurs cycles à s’exécuter avec un pipeline. Les instructions immédiatement après le branchement sont chargées dans le pipeline à sa suite, à raison d'une instruction par cycle. Ces instructions sont chargées à tort et il faut éliminer ce problème.
Pour corriger les problèmes liés aux branchements, il existe une solution purement logicielle, qui ne demande pas le moindre ajout de matériel. L'idée est de faire suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un délai de branchement. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Par exemple, si l'adresse à laquelle brancher est connue avec deux cycles d’horloge de retard, il suffit de placer deux NOP juste après le branchement. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité, sans compter que la perte de performance est notable.
D'autres techniques purement matérielles permettent de corriger les problèmes causés par les branchements, sans recourir à des délais de branchements. Ce qui permet de gagner en performance, tout en corrigeant les problèmes de compatibilité. Et ces techniques demandent d'annuler les instructions chargées à tort. Mais pour cela, il faut que les écritures réalisées par ces instructions se fassent dans l'ordre. Dans ce chapitre, nous allons voir les techniques utilisées pour gérer les problèmes liés aux branchements. De plus, nous allons voir comment ces techniques font pour garantir l'ordre des écritures et donc éliminer les dépendances WAW, y compris sur les pipelines dynamiques. Nous ferons donc d'une pierre deux coups.
Les branchements : annuler les instructions chargées à tort à l'étape de décodage
[modifier | modifier le wikicode]La première solution que nous allons voir consiste à identifier les branchements à l'étape de décodage. Si l'étape de décodage décode un branchement, elle sait qu'elle a potentiellement chargé des instructions à tort. Après, tout dépend de si le branchement est conditionnel ou non. Les deux cas sont gérés à part, mais pour une raison totalement différente. Le point est qu'on sait si le branchement est pris dès l'étage de décodage, alors que l'on doit faire un calcul dans l'ALU avec les branchements conditionnels.
Les solutions que nous allons voir sont légèrement différentes selon que l'on parle des interruptions/exceptions matérielles, des branchements conditionnels ou inconditionnels. En effet, on doit tenir compte de deux cas : est-ce que le branchement est pris ou non. Si le branchement n'est pas pris, aucun problème : les instructions n'ont pas été chargées à tord. Par contre, ce n'est pas le cas pour les branchements pris. Rappelons que certains branchements sont toujours pris : les branchements inconditionnels, les appels de fonctions, les interruptions logicielles. Par contre, les branchements conditionnels peuvent être pris ou non-pris.
Les branchements inconditionnels : annuler les instructions chargées à tord
[modifier | modifier le wikicode]Le premier cas est celui des branchements inconditionnels et des interruptions logicielles. Je rappelle que les interruptions logicielles sont des instructions machine, mais pas interruptions matérielles ou les exceptions. Branchements inconditionnels et interruptions logicielles sont systématiquement pris, et on le sait dès l'étape de décodage. Le dernier point est important, car il simplifie grandement l'implémentation. Avant l'étape de décodage, aucun registre architectural n'a été modifié, il n'y a pas eu d'accès à la mémoire, l'état du processeur n'a pas changé. Il suffit de ne pas exécuter les instructions chargées à tort pour régler le problème. Il n'y a donc pas lieu de revenir en arrière et de corriger ce qu'on fait les instructions chargées à tort.
Le processeur est certain qu'il a chargé à tord des instructions. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Dans ce cas, par sécurité, les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. Notons qu'il ne s'agit pas de bulles de pipeline. Une bulle de pipeline retarde une instruction, elle la bloque dans l'unité de décodage. Ici, les instructions sont annulées. Le processeur restaure le program counter à sa valeur adéquate naturellement, en exécutant le branchement. L'implémentation demande juste un simple compteur et quelques circuits annexes. Évidemment, on perd un peu en performances comparé à un cas idéal, mais n'a pas le choix. Nous verrons dans quelques chapitres que des techniques de prédiction de branchement permettent de résoudre ce problème, mais laissons cela de côté pour le moment.
Les branchements conditionnels : émettre des bulles de pipeline en plus
[modifier | modifier le wikicode]Pour les branchements conditionnels, la technique doit être quelque peu adaptée pour tenir compte du cas où le branchement est non-pris, mais les modifications sont mineures. Le cas des branchements conditionnels est résolu avec l'ajout de bulles de pipeline à la technique précédente. L'idée est de ne pas exécuter d'instruction tant que le résultat du branchement n'est pas connu. Une fois le résultat connu, on décide s'il faut annuler ou non les instructions chargées à tord. Il y a une petite subtilité quant à l'envoi des bulles de pipeline. Quand le processeur décode un branchement, il envoie le branchement aux unités de calcul. Il émet des bulles de pipeline immédiatement après. Les bulles de pipeline commencent à l'instruction suivante. En clair, les instructions qui suivent le branchement sont bloquées à l'étape de décodage, en attendant son résultat.
L'idéal est de connaitre le résultat du branchement à l'étage de décodage, car cela permet une implémentation similaire à celle des branchements inconditionnels, sans bulle de pipeline. Mais les branchements conditionnels demandent qu'une condition soit calculée dans les unités de calcul, soit bien après l'étage de décodage. La méthode ne s'applique donc pas. Sauf qu'il y a quelques exceptions.
La première solution consiste à calculer les branchements pendant l'étape de décodage. Elle demande d'ajouter une unité de calcul spécialisée dans les branchements, qui travaille en parallèle de l'unité de décodage. La mini-ALU fournit un résultat pendant l'étape de décodage. Le résultat du branchement est alors connu pendant l'étape de décodage, qui peut faire son travail. Le défaut de cette technique est que l'unité de calcul en question doit lire les registres. La solution en question est utilisée sur le pipeline RISC classique. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage.
La seconde solution marche si les branchements conditionnels sont implémentés de manière à ne pas calculer de condition, mais se contentent de lire un registre d'état ou un registre de prédicat pour savoir s'ils sont pris ou non. Le jeu d'instruction doit alors séparer instructions de test et branchements proprement dits. Mais dans les faits, de tels branchements conditionnels ne sont pas souvent utilisés sur les processeurs avec un pipeline. La raison est que le branchement lit un registre que le test écrit. Et le résultat de l'instruction de test met souvent plusieurs cycles pour s'exécuter. En conséquence, le branchement doit être bloqué dans l'unité de décodage durant quelques cycles, le temps que le registre d'état/à prédicat soit mis à jour par l'instruction de test. On ne fait que remplacer des délais de branchement par des bulles de pipeline. La solution n'est pas très pratique.
Les branchements et exceptions sur un pipeline de longueur fixe
[modifier | modifier le wikicode]Maintenant, nous allons voir une solution qui ne s'applique que sur les pipelines de longueur fixe. Elle a pour particularité qu'elle annule les instructions chargées à tord à la toute fin du pipeline, lors de l'étape d'enregistrement dans les registres. L'idée est que si une instruction n'enregistre pas son résultat, c'est comme si elle n'avait rien fait. Et le seul moyen pour elle d'enregistrer son résultat est de l'enregistrer dans les registres, ou en mémoire. Donc, si on coup l'enregistrement dans les registres à la fin du pipeline, alors le problème est résolu : les instructions fautives sont annulées.
- Les écritures en mémoire sont donc à part, car elles n'écrivent pas en mémoire à la fin du pipeline. Elles sont donc traitées autrement. Typiquement, elles sont retardées si elles suivent un branchement conditionnel.
Les exceptions précises
[modifier | modifier le wikicode]Pour aborder la technique, voyons ce qu'il en est pour les exceptions matérielles. Rappelons qu'une interruption ou une exception matérielle stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Il s'agit de branchements cachés, ce qui fait que le même problème se manifeste. Avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline alors qu'elles n'auraient pas dû l'être, à savoir les instructions qui sont placées après l'instruction à l'origine de l'exception dans l'ordre du programme. Logiquement, elles n'auraient pas dû être exécutées, vu que l'exception est censée faire brancher le processeur autre part immédiatement.
Le résultat est que les exceptions et interruptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique que le program counter au moment où se déclenche l’exception est celui de cette instruction. Or, avec un pipeline, vu que plusieurs instructions ont été chargées en avance, ce n'est pas le cas.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
L'implémentation de l'annulation des instructions à l'étape d'enregistrement
[modifier | modifier le wikicode]Pour redémarrer l'instruction fautive, les exceptions doivent être précises, c'est à dire que l'état du processeur ne doit pas avoir été modifié après que l'exception ait eu lieu l'exception. Pour cela, il faut que le program counter de l'instruction ait été mémorisé quelque part et soit restauré. Il fait de plus que les instructions qui suivent l'exceptions aient été annulées, que les registres n'aient pas été modifiés par les instructions suivants l'exception. Sans cela, il se pourrait que le re-démarrage de l'instruction donne un résultat différent de celui attendu.
Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Tout étage fournit à chaque cycle un indicateur d'exception, un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et autorise ou interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée.
De plus, il faut restaurer le program counter pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le program counter est propagé dans le pipeline et est restauré en cas d'exception en prenant le program counter disponible à l'étage d'enregistrement.
Le hardware précédent peut être utilisé pour gérer les branchements inconditionnels et conditionnels. Les branchements inconditionnels sont généralement gérés sans : ne pas exécuter les instructions chargées à tort est une meilleure solution niveau performance. Mais pour les branchements conditionnels, tout change vu que le résultat du branchement n'est pas connu à l'étape de décodage. L'idée est là aussi de ne pas enregistrer les résultats des instructions fautives. Les instructions chargées à tord continuent à se propager dans le pipeline, mais elles échouent à la dernière étape. L'avantage est que le décodeur n'a pas à faire attendre les instructions qui suivent le branchement dans le décodeur à grand coup de bulles de pipeline. Elles sont annulées si le branchement est pris, mais n'auront pas été retardées si le branchement n'est pas pris. Le gain en performance est bon à prendre.
En théorie, on devrait faire la différence entre les exceptions avant et après l'étape de décodage. La solution utilisée pour les branchements inconditionnels est censée marcher avec les exceptions survenue avant décodage. Mais dans les faits, cela rajouterait des complications pour pas grand chose. A la place, toutes les exceptions sont traitées de la même manière, dans l'étage final du pipeline. Les exceptions sont assez rares, aussi pas besoin de faire de différence pour les traiter au plus vite. De plus, il faut absolument traiter les exceptions dans l'ordre de survenue, et séparer exceptions avant et après décodage n'aide pas trop.
Les branchements et exceptions précises sur un pipeline de longueur variable
[modifier | modifier le wikicode]Passons maintenant aux pipelines de longueur variable. La situation est alors plus complexe, pour plusieurs raisons. La première est que de tels pipeline ne garantissent pas l'ordre des écritures. Les instructions sont émises une par une, mais fournissent leur résultat dans le désordre si elles n'ont pas de dépendances entre elles. Elles quittent l'unité d'émission une par une, dans l'ordre, mais les enregistrements dans les registres se font dans le désordre. Or, cela empêche de restaurer l'état initial du processeur après un branchement. Un autre problème survient avec les exceptions matérielles. Les écritures dans les registres ont lieu dans un ordre différent de celui imposé par le programme, ce qui pose problème quand on veut reprendre là où l'exception arrêté le programme.
Mais il y a une solution toute simple : exécuter les instructions dans le désordre, mais forcer les écritures dans les registres l'ordre naturel du programme. Les écritures dans les registres sont donc retardées, tant que les instructions précédentes ne sont pas terminées. Une autre solution effectue les écritures immédiatement, mais corrigent les écritures erronées en cas de problème. Les instructions démarrent dans l'ordre du programme, mais les plus rapides peuvent écrire leur résultat avant les autres.
Toutes ces techniques demandent d'ajouter un étage de pipeline pour remettre les écritures dans l'ordre du programme. Celui-ci est inséré entre l'étage d’exécution et celui d'enregistrement. De plus, le processeur doit mémoriser l'ordre d'émission des instructions en cours dans une mémoire spéciale, à mi-chemin entre mémoire FIFO et mémoire associative. La mémoire en question varie suivant la technique utilisée, elles portent des noms très particulier : un tampon de réordonnancement, un result shift register, etc.
Toujours est-il que ces mémoires contiennent des entrées, des sortes de cases mémoires qui contiennent tout ce qu'il faut mémoriser sur une instruction. Le contenu d'une entrée dépend de la méthode utilisée, mais elle mémorise généralement le registre de destination de la donnée et un identifiant d'instruction (typiquement le program counter). Les entrées sont allouées dans l'ordre d'émission, ce qui fait que les instructions rentrent dans la mémoire à l'étage d'émission. Elles ne sortent quand leur résultat est connu et que toutes les instructions précédentes dans l'ordre du programme se sont exécutées. Il s'agit donc d'une mémoire FIFO. Seulement, il arrive que le contenu d'une entrée soit modifié entre temps.
Le result shift register
[modifier | modifier le wikicode]Une première implémentation se charge de mettre en attente les écritures en utilisant un result shift register. Ne vous fiez pas à son nom, il s'agit en réalité d'une mémoire FIFO tout bête. Chaque byte de la mémoire mémorise toutes les informations pour décider quand écrire le résultat de l'instruction dans les registres, et comment configurer l'étape d'enregistrement dans les registres.
Elle mémorise : l'unité de calcul utilisée, le registre de destination, et son program counter. L'unité de calcul est utilisée pour savoir comment configurer le multiplexeur qui relie les sorties des ALU au port d'écriture du banc de registres. Cela suppose que le résultat est maintenu en sortie de l'unité de calcul, dans un registre tampon sur cette sortie, tant qu'il n'est pas écrit dans les registres. Le registre de destination sert à configurer le port d'écriture du banc de registres. Le program counter sert surtout pour la gestion des interruptions et exceptions précises. L'ensemble est mémorisé dans un byte, aussi appelé une entrée du result shift register.
Les instructions sont insérées dans ce result shift register dans l'étage d'émission. Si une instruction prend N cycles pour s’exécuter, elle est placée dans le byte numéro N. A chaque cycle, les entrées sont déplacés, décalés : l'instruction passe de l'entrée N à l'entrée N-1. La mémoire se comporte donc comme une sorte de registre à décalage, ou mieux : comme une mémoire FIFO à l'adressage bizarre. Les instructions quittent cette mémoire dans l'ordre des entrées : l'entrée sortante sert à configurer les MUX et le port d'écriture.
Le tampon de réordonnancement
[modifier | modifier le wikicode]Le tampon de réordonnancement est une technique complémentaire de la précédente, où on insère une mémoire tampon dans laquelle es écritures se font, avant d'être envoyés dans les registres. La technique précédente forçait les résultats à attendre en sortie de l'unité de calcul, si nécessaire. La technique du tampon de réordonnancent fusionne ces registres dans une seule mémoire tampon insérée avec le banc de registres. Le tampon de réordonnancent s'appelle le Re-order buffer en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit.
Les résultats des instructions quittent les unités de calcul dès que possible, mais ils ne sont pas écrits directement dans les registres ou la RAM. A la place, ils sont mémorisés dans le tampon de réordonnancent. Ils quitteront le tampon de réordonnancent dans l'ordre du programme, comme s'il n'y avait pas d’exécution dans le désordre, afin que les résultats soient envoyés aux registres dans le bon ordre.
Un résultat est enregistré dans un registre lorsque les instructions précédentes (dans l'ordre du programme) ont toutes elles-mêmes enregistré leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. Pour garantir que les instructions sont triées dans l'ordre du programme, les instructions sont ajoutées dans le ROB à l'étape d'émission. Les instructions étant émises dans l'ordre du programme, l'ajout des instructions dans le tampon de réordonnancent se fait automatiquement dans l'ordre du programme.
Le tampon de réordonnancement est un mélange entre une mémoire FIFO et une mémoire associative. En effet, il faut non seulement insérer les résultats dans le tampon de réordonnancent, mais il faut aussi le faire dans le bon ordre. Pour cela, il faut faire le lien entre un résultat et une instruction, ce qui ressemble un petit peu à ce que fait une mémoire cache, d'où le fait que le tampon de réordonnancent ressemble un peu à une mémoire associative.
Le ROB est une mémoire de taille variable, c'est à dire qu'on peut y ajouter un nombre variable d'instructions, jusqu'à une limite maximale. Si la limite maximale est atteinte, le ROB est plein et on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc.
Un point important est que les données stockées dans le ROB sont certes mises en attente, mais qu'il est possible que certains calculs en aient besoin. Par exemple, imaginons que le processeur veuille émettre une instruction dont une opérande est dans le ROB. L'opérande a été calculée, mise en attente dans le ROB, mais n'est pas encore enregistrée dans le banc de registre. La politique diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Mais pour faire ainsi, il faut que le processeur implémente des techniques dites de renommage de registre que nous verrons dans quelques chapitres.
Les entrées du ROB
[modifier | modifier le wikicode]Les entrées du ROB contiennent de quoi faire le lien entre un résultat et l'instruction associée. Il est possible de le voir comme un result shift register amélioré, sur lequel on aurait ajouté des informations en plus dans chaque entrée. Ce qui fait qu'on retrouve le program counter associé à l'instruction, le registre de destination du résultat, et quelques autres informations. Mais à cela il faut ajouter de quoi mémoriser le résultat, ce qui demande de mémoriser : le résultat de l'instruction, écrit dans un champ initialement laissé vide, et un bit de présence qui est mis à 1 quand le résultat de l'instruction est écrit dans l'entrée. Ce dernier sert à indiquer que le résultat a bien été calculé.
A cela, certaines implémentation ajoutent un bit Exception qui précise si l'instruction a levé une exception ou non. Lorsqu'un résultat quitte le ROB, pour être enregistré dans les registres, le bit Exception est vérifié pour savoir s'il faut ou non vider le ROB. Si une exception a lieu, le ROB se débarrasse des instructions qui suivent l'instruction fautive (celle qui a déclenché l'interruption ou la mauvaise prédiction de branchement) : ces résultats ne seront pas enregistrés dans les registres architecturaux.
Pour rappel, certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire. La logique voudrait que ces instructions ne prennent pas d'entrée dans le ROB. Mais n'oubliez pas qu'on détermine à quelle adresse reprendre en se basant sur le program counter de l'instruction qui quitte le ROB : ne pas allouer d'entrées dans le ROB à ces instructions risque de faire reprendre le processeur quelques instruction à côté. Pour éviter cela, on ajoute quand même ces instructions dans le ROB, et on rajoute un champ qui stocke le type de l'instruction, afin que le ROB puisse savoir s'il s'agit d'une instruction qui a un résultat ou pas. On peut aussi utiliser cette indication pour savoir si le résultat doit être stocké dans un registre ou dans la mémoire.
Rappelons qu'un result shift register suppose que le résultat est maintenu en sortie de l'unité de calcul, dans un registre tampon sur cette sortie. Et cela se marie bien avec la technique du contournement. Mais avec un ROB, les choses sont plus compliquées. Il est possible de garder un réseaux d'interconnexions entre ALU pour gérer le contournement. Mais on peut aussi modifier le tout de manière à récupérer les opérandes à contourner dans le ROB directement. Ainsi, les résultats des instructions sont écrites dans le ROB, et peuvent être lues dedans directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont alors fusionné avec le ROB, ce qui colle bien avec sa nature de mémoire associative. Évidemment, les deux solutions peuvent être mélangées : on peut faire du contournement avec des interconnexion directes entre ALU, et contourner avec le ROB, les deux ne sont pas incompatibles, mais complémentaires. Par exemple, les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes.
Le tampon d’historique
[modifier | modifier le wikicode]Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le tampon d’historique (history buffer ou HB).
Comme pour le ROB, le HB est une mémoire FIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception.
Le banc de registres futurs
[modifier | modifier le wikicode]Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc.
Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le banc de registres futurs (future file ou FF). Il fonctionne plus ou moins comme l'History Buffer de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le banc de registres de retrait (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit.
Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct.
Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.*
Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres.
Les point de contrôle de registre
[modifier | modifier le wikicode]La technique du register checkpointing est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements.
Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de point de contrôle de registre, nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur.
Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde.
Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc.
La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF.
La complétion dans le désordre
[modifier | modifier le wikicode]Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de complétion dans le désordre.
En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du retirement register file, du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le scoreboard ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté.
Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il s’applique en absence de branchements et peut être vu comme une amélioration des lectures non-bloquantes. Le mécanisme s'active quand une lecture est bloquée par un défaut de cache ou toute autre situation. Le processeur implémente l'exécution dans le désordre, ce qui fait qu'il exécute les instructions qui suivent une lecture bloquée, à condition qu'elles ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en attente. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon.
Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU.