Aller au contenu

Fonctionnement d'un ordinateur/Le contournement (data forwarding)

Un livre de Wikilivres.

Les dépendances RAW correspondent au cas où une instruction a pour opérande le résultat d'une instruction précédente. Vous vous dites que le résultat d'une opération doit être enregistré dans les registres avant de pouvoir être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Pour gérer les dépendances RAW, on a deux grandes solutions. La première solution est de retarder l'exécution de la seconde instruction tant que la première n'a pas enregistré son résultat dans les registres, avec des bulles de pipeline. La technique du contournement (bypass) rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. Le présent chapitre est dédié uniquement à cette technique dite de contournement.

Pipeline Bypass

Le contournement sur les pipelines 1-cycle

[modifier | modifier le wikicode]

Dans cette section, on étudie un pipeline 1-cycle, ce qui veut dire que toutes les instructions s'exécutent en un seul cycle d'horloge. De plus, il n'y a qu'une seule unité de calcul, combinée avec une unité d'accès mémoire. La raison est que les instructions multicycles compliquent la mise en place du contournement. Il en est de même avec la présence de plusieurs unités de calcul.

Le contournement de l'unité de calcul

[modifier | modifier le wikicode]

La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée.

Principe du contournement avec le pipeline RISC classique.

Pour cela, on connecte la sortie de l'unité de calcul sur son entrée si besoin, et on la déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs.

Contournement avec des multiplexeurs.

Pour détecter les dépendances, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul.

Implémentation complète du contournement

Le contournement des lectures mémoire

[modifier | modifier le wikicode]

La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand unelecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type load-op, qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué).

Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les instructions lecture-dépendantes, sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre, mais cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire.

Implémentation du contournement de l'unité mémoire

La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le l'unité de contournement. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs.

Implémentation complète du contournement mémoire avec un multiplexeur

Mais ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt.

Data Forwarding (Two Stage, error)

La solution est de retarder le résultat d'un cycle d'horloge en insérant une bulle de pipeline avant de démarrer l'instruction de calcul. En clair, on économise un cycle au lieu de deux.

Data Forwarding (Two Stage)

La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour.

Le contournement des écritures mémoire

[modifier | modifier le wikicode]

Après avoir vu le contournement pour les lectures, voyons maintenant le contournement des écritures. Au premier abord, cela peut paraitre paradoxal. Une écriture ne fournit pas de résultat, il n'y a rien à contourner. Cependant, il faut prendre en compte le fait que le processeur gère des exceptions précises. Pour rappel, les exceptions précises imposent que les écritures se fassent à la toute fin du pipeline, dans l'étage de Writeback. Pour cela, les écritures sont sont mises en attente jusqu'à l'étage de Writeback.

Pour rappel, la mise en attente se fait grâce à une mémoire spécialisée, appelée la file d'écriture. C'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre normal des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de Writeback transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de Writeback. Si une exception est détectée, l'étage de Writeback vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées.

File d'écriture sur un pipeline fixe

Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Le cas est rare, car il demande de lire une donnée qu'on vient d'écrire. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Il s'agit bien d'une dépendance RAW assez particulière. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide.

Il est possible d'éviter cela à l'émission, en ajoutant des bulles de pipeline, mais cela demande de comparer des adresses et c'est clairement compliqué. Aussi, une autre solution utilise une forme de contournement pour résoudre ce problème. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un succès de file d'écriture, et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de Store to load Forwarding ou encore de réacheminement écriture-vers-lecture.

Store to load forwarding sur un pipeline simple

Le contournement sur les pipelines multi-cycle

[modifier | modifier le wikicode]

L'usage d'instructions multicycles complexifie grandement d'implémentation du contournement. Et il y a plusieurs raisons à cela. La première est la présence de plusieurs unités de calcul, qui complexifie les interconnexions. La seconde est que la durée des instructions est à prendre en compte pour effectuer un contournement convenable.

La durée des instructions est à prendre en compte

[modifier | modifier le wikicode]

Les pipelines multicycles peuvent en théorie se passer de bulles de pipeline grâce au contournement. Ce n'est plus le cas en présence d'instructions multicycles. Un exemple parlant est celui où une multiplication de 5 cycles est suivie par une addition d'un cycle. L'addition a pour opérande le résultat de la multiplication. Les deux instructions ne peuvent pas être émises l'une après l'autre sans bulle de pipeline. Le résultat de la multiplication est disponible après 5 cycles, l'addition doit attendre 4 cycles avant d'être émises, sans quoi elle entrera dans l'ALU trop tôt, avant que le multiplieur fournisse le résultat voulu.

L'unité d'émission doit retarder l'émission d'une instruction tant que le résultat n'est pas disponible, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas.

La présence de plusieurs ALU et le réseau de contournement

[modifier | modifier le wikicode]

Le premier problème est la présence de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque ALU sur l'entrée de tous les autres. Le nombre d'interconnexion est alors assez important. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2 * N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul. A la place, les interconnexions sont alors simplifiées, au prix d'une petite perte de performance.

L'ensemble des interconnexions pour le contournement s'appelle le réseau de contournement. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires.

Il est fréquent que les unités de calcul d'adresse utilisent des opérandes calculées par les ALU entières. En conséquence, il faut relier les sorties des ALU entières aux unités de calcul d'adresse. Cela fait des interconnexions nécessaires, qui ont un impact important pour les performances. Mais les connexions inverses, de l'ALU de calcul d'adresse vers l'ALU entière, ne servent pas à grand chose. La connexion ALU-AGU se fait dans un sens seulement.

Dans le même genre, la sortie de l'unité mémoire est reliée aux entrées de toutes les autres unités de calcul, pas le choix. Mais cela dépend un peu du processeur.

Le contournement doit prendre en compte la présence d'une FPU si elle existe. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un avec la ou les FPUs. Il arrive que le processeur ait plusieurs FPUS et c'est la même chose, le réseau de contournement entre FPUs est juste plus complexe.

Réseau de contournement avec plusieurs ALU

Le contournement avec remise en ordre des écritures

[modifier | modifier le wikicode]

Nous avons vu il y a quelques chapitres que les pipelines dynamiques on un problème d'inversion des écritures. Deux instructions peuvent s'exécuter l'une après l'autre et enregistrer leurs résultats dans l'ordre inverse. Mais les processeurs incorporent souvent des techniques pour remettre les écritures dans l'ordre, qu'il s'agisse de registres d'échelonnement, d'un tampon de ré-ordonnancement, etc. L'idée est que les résultats à écrire ne sont pas enregistrés dans les registres directement, mais sont mis en attente dans une mémoire temporaire, pour être enregistrés dans les registres seulement quand les conditions sont réunies. Nous ferons quelques rappels sur cette mémoire dans ce qui suit.

La présence de cette mémoire temporaire perturbe un peu le contournement. Elle n'impacte pas les interconnexions entre unités de calcul. Mais le contournement devrait en théorie lire les opérandes depuis cette mémoire temporaire. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi.

Le contournement avec les registres d'échelonnage

[modifier | modifier le wikicode]

La première technique que nous allons voir est celle des registres d'échelonnage. Pour rappel, l'idée est que toutes les instructions prennent le même nombre de cycles d'horloge. Par exemple, toutes les opérations doivent faire 5 cycles. Si l'addition fournit son résultat en un cycle, il sera enregistré dans les registres avec un retard de quatre cycles. Le retard dépend de l’opération à effectuer, en fonction de combien de cycles elle prend dans l'ALU. En clair, toutes les instructions ont le même nombre d'étages, mais certains étages sont inutiles pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire.

Reste à ajouter des cycles de retard, qui servent juste à retarder l'enregistrement dans les registres. La solution la plus simple insére des staggering registers, des registres d'échelonnage, en sortie des ALU. Les registres d'échelonnage peuvent être regroupés dans un banc de registre unique.

Regsitres d'échelonnage
Banc de registres d'échelonnage

Pour les pipelines de longueur fixe, le contournement ne doit pas uniquement relier la sortie de l'ALU, mais doit aussi tenir compte des registres d'échelonnage. En effet, imaginez qu'une instruction ait pour opérande le résultat d'une instruction exécutée deux cycles plus tôt. Il y a un délai de deux cycles entre les deux, mais l'ALU a fournit un résultat en un cycle d'horloge. Où est le résultat voulu ? Il n'est pas en sortie de l'ALU, mais dans le registre d'échelonnage juste après. Le contournement doit donc relier les registres d'échelonnage aux entrées des unités de calcul.

Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Mais faire ainsi nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et insère des bulles de pipeline si besoin.

Le contournement avec un tampon de ré-ordonnancement

[modifier | modifier le wikicode]

Les processeurs modernes ajoutent un tampon de ré-ordonnancement, pour remettre les écritures dans les registres dans l'ordre du programme. Les données à écrire sont mise en attente dans le tampon de ré-ordonnancement, qui est une sorte d'hybride entre mémoire cache et mémoire FIFO.

Tampon de réordonnancement.

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 un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction 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.

Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail.

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.

L'émission anticipée des micro-opérations

[modifier | modifier le wikicode]

Idéalement, on voudrait démarrer une nouvelle instruction sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU.

Si une instruction réutilise le résultat d'une autre, cela peut poser problème. Imaginons que l'on ait trois cycles entre la file d'attente et les ALU. Si une instruction est émise quand l'opérande sorte de l'ALU, elle prendrait plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Et pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d'émission anticipée.

Le nombre de cycles d'avance dépend de l'opération, précisément du temps d'exécution de l'opération dans l'ALU. Le cas le plus simple est celui d'une opération qui s’exécute en un seul cycle d'horloge, comme une addition ou une soustraction. Le nombre de cycles d'avance est alors le nombre d'étages entre l'unité de calcul et la fenêtre d'instruction. Pour une instruction multicycle, il faut ajouter le nombre de cycles que l'opération passe dans l'ALU (moins un cycle). La conséquence est que cela ne marche que pour des instructions qui ont une durée fixe, connue à l'avance. Et autant c'est le cas pour les instructions arithmétiques et logiques, autant ce n'est pas le cas pour les accès mémoire.

L'émission anticipée des accès mémoire

[modifier | modifier le wikicode]

Plus haut, nous avons vu comment appliquer le contournement à l'unité mémoire. Mais cela ne marche que dans un cas simple, où une lecture fait un cycle d'horloge. Dans les processeurs modernes, les accès mémoire ont une latence variable : quelques cycles pour un succès de cache, plusieurs dizaines de cycles pour un défaut de cache L1, pas loin de la centaine pour un défaut de cache L2, etc. Et cela pose des problèmes pour l'émission anticipée et le contournement en général.

L'émission en avance pour les accès mémoire est purement spéculative : le processeur émet des instructions en supposant que la lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont alors émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Et pour corriger la situation, il y a plusieurs solutions.

Le pipeline RISC classique gèle totalement le processeur en cas de défaut de cache. Les instructions restaient à leur place, elles ne bougeaient pas, et le processeur reprenait là où il en était une fois le défaut de cache terminé. Mais cette solution, bien qu'idéale, n'est pas la plus pratique. Une solution alternative ne gèle que l'instruction dépendante sur place, dans le pipeline. L'instruction dépendante attend en entrée de l'unité de calcul que l'opérande nécessaire soit disponible, que le défaut de cache se passe. Le problème est que tout ce qui précède l'unité de calcul doit aussi être gelé : l'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Le défaut de cette solution est qu'elle est difficile à mettre en œuvre, sauf à geler totalement le processeur comme le faisaient les processeurs avec un pipeline RISC classique, comme dit plus haut.

Une autre solution annule toutes les instructions après celles émise à tort, avec les circuits pour les exceptions précises. Un défaut de cache déclenche une exception matérielle interne au processeur. Le processeur élimine alors les toutes instructions émises après le défaut de cache, dont les instructions lecture-dépendantes mais pas que. Une autre solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l'annulation sélective. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre.

Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de prédiction de latence mémoire tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités.

Le replay pipeline du Pentium 4

[modifier | modifier le wikicode]

Le pipeline à répétition (replay pipeline) est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande.

L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un scoreboard, et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ».

Pipeline à répétition.

Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de replay, l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de replay autant de fois que nécessaire.

Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal cache hit/miss.

La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance.
Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.

Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente.

Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple).

Pipeline à répétition pour une hiérarchie de cache.

Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps.

Gestion des accès RAM sur un pipeline à répétition.

On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission.