Fonctionnement d'un ordinateur/Les pipelines de longueur variable

Les pipelines précédents sont des pipelines de longueur fixes, où toutes les instructions s’exécutent en un nombre fixe de cycles. Et quand je dis toutes les instructions, c'est vraiment toutes les instructions, sauf les accès mémoire qui sont traités à part. De tels pipelines sont classés en deux types : les pipelines 1-cycle, où toutes les instructions s'exécutent en un seul cycle, et les pipelines multicycle ou toutes les instructions font plusieurs cycles.

Sur ces derniers, les instructions doivent se caler sur l'instruction la plus lente. Si une addition met un cycle à s'exécuter et une multiplication 3, alors toutes les instructions font 3 cycles. Autant dire que les performances sont pas terribles. Idéalement, on souhaiterais que l'addition ne prennent qu'un seul cycle, quitte à enregistrer son résultat dans les registres en avance. Et c'est là qu'interviennent les pipelines de longueur variable, où certaines instructions prennent plus de cycles que d'autres. Un autre terme, peu utilisé, est celui de pipeline dynamique, terme pus court qu'il nous arrivera d'utiliser. Le chapitre entier leur est dédié.

Les pipelines dynamiques : la séparation entre front-end et back-end

modifier

Avec un pipeline dynamique, le processeur est décomposé en plusieurs parties. La première partie, l’amont (front end), prend en charge les étages communs à toutes les instructions : la mise à jour du program counter, le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, découpé en plusieurs unités adaptées à un certain type d'instructions, chacune formant ce qu'on appelle un aval (back end). Un aval est spécialisé dans une classe d'instructions : un pour les instructions arithmétiques et logiques, un autre pour les opérations d'accès mémoire, un autre pour les instructions d'échange de données entre registres, et ainsi de suite.

 
Pipeline avec un aval et un amont (back-end et front-end).

Un aval est une unité de calcul/mémoire

modifier

Pour simplifier, un aval est soit une unité de calcul, soit une unité d'accès mémoire. Toutes les unités de calcul sont des avals potentiels. Et dans ce qui suit, nous allons partir du principe que le processeur est organisé de manière à ce que ce soit le cas. Nous verrons que certains processeurs superscalaires n'utilisent pas vraiment cette méthode, mais nous verrons cela en temps voulu.

Il est possible de se contenter de deux avals : un pour les accès mémoire et un pour les calculs. L'unité de calcul est alors utilisée en parallèle de l'unité de communication avec la mémoire.

 
Pipeline à deux aval de type load-store.

On peut aussi utiliser une unité de calcul séparée pour les instructions de tests et branchements.

 
Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.

Il est possible de scinder les avals précédents en avals plus petits. Par exemple, l'aval pour les instructions arithmétiques peuvent être scindés en plusieurs avals séparés pour l'addition, la multiplication, la division, etc. Niveau accès mémoire, certains processeurs utilisent un aval pour les lectures et un autre pour les écritures. De même, on a un aval différent pour les instructions entières et les instructions flottantes. La longueur de l’aval varie suivant le type d'instructions.

 
Pipeline avec un nombre variable d'étages par instructions.

Le partage du banc de registre entre avals

modifier

Le pipeline RISC classique avait une version disposant d'instructions multicycles. Elle implémentait les instructions multicycles dans un aval séparé des avals pour instruction 1-cycle. Les instructions multicycles regroupent globalement les multiplications et éventuellement les divisions, ou d'autres instructions arithmétiques assez gourmandes. Elles disposaient de leur propre set de registres rien que pour elles. Les instructions multicycle s’exécutaient en parallèle dans une unité de calcul spécialisée, et enregistraient leurs résultats dans un banc de registre séparé. Cela résolvait pas mal de problèmes de conflits d'accès aux registres.

Mais les processeurs dynamiques n'utilisent généralement pas cette solution. Les instructions multicycle lisent et écrivent dans le même aval que les instructions 1-cycle. Le résultat est qu'elles peuvent lire ou écrire dans les registres en même temps. Par exemple, il arrive que deux instructions se mettent à faire des écritures au même moment. Par exemple, imaginons que l'on charge dans le pipeline une instruction de 6 cycles, suivie au cycle suivant par une qui n'en prend que 5. Elles tenteront d'enregistrer leurs données en même temps 6 cycles après pour la première, 1 + 5 cycles pour l'autre. La seule solution est que la seconde instruction doit être retardée d'un cycle pour résoudre le problème. En soi, rien de problématique à gérer, l'unité de décodage a juste à insérer des bulles de pipeline.

Les dépendances de données sur un pipeline de longueur variable

modifier

L'usage de plusieurs pipelines de longueur variables pose divers problèmes. Le problème principal est que si on émet des instructions dans l'ordre du programme, il se peut qu'elles ne terminent pas dans le même ordre. Par exemple, prenons l'exemple où une instruction multicycle de 6 cycles, est suivie par une instruction s’exécutant en un seul cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé.

 
Exemple de problème survenant avec des pipeline de longueur variable

Imaginez par exemple que la seconde instruction ait besoin des résultats de la première pour s’exécuter : la seconde instruction aura lu des registres pas encore mis à jour, et donnera un résultat incorrect. Les problèmes de ce type surviennent quand deux instructions dans le pipeline ont une ou plusieurs dépendances de données. Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) au même registre ou à la même adresse mémoire. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde instruction ne peut pas s’exécuter tant que la première n'a pas enregistré son résultat dans les registres, et doit rester dans l'unité de décodage en attendant. C'est le cas le plus évident, mais on a aussi des cas moins intuitifs.

Une description/classification des dépendances de données

modifier

Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) au même registre ou à la même adresse mémoire. Dans ce qui va suivre, nous allons nous limiter au cas où les instructions ont pour seules opérandes des registres, mais pas des adresses mémoires, ce qui est le cas pour les instructions de calcul d'un processeur LOAD-STORE. Le cas des dépendances mémoire fera l'objet d'un chapitre à part sur la désambiguïsation mémoire. Et les processeurs que nous avons vu pour le moment cessent de décoder de nouvelles instructions tant qu'un accès mémoire est en cours, ils laissent juste les instructions précédentes finir. Avec un tel processeur, les dépendances problématiques sont les suivants :

  • Read after write : Une instruction lit un registre dans laquelle l'instruction précédente a écrit.
  • Write after read : Une instruction lit un registre pour récupérer une opérande, mais ce registre est ensuite écrasé par une instruction ultérieure.
  • Write after write : Deux instructions écrivent dans le même registre, mais à des instants différents.

Le cas Read after read n'est techniquement pas une dépendance. On peut mettre les deux instructions dans n'importe quel ordre, cela ne pose aucun problème. Ce ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, ce n'est pas le cas avec les trois autres types de dépendances, qui imposent d’exécuter la première instruction avant la seconde. Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW.

Les dépendances WAR et WAW sont souvent appelées des dépendances de nom (naming dependency). Elles sont opposées aux dépendances vraies (true dependency), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Et on peut les faire disparaitre avec des techniques dites de renommage de registre, qui feront l'objet d'un chapitre ultérieur.

Dans ce chapitre, nous n'allons pas tenir compte des dépendances WAR. Sur la plupart des pipelines dynamiques, les dépendances WAR n'existent tout simplement pas. La raison est que pour viller une telle dépendance, une écriture doit être déplacée avant une lecture. Or, les données sont lues au tout début du pipeline et les écritures ont lieu à la fin. Il faut vraiment chercher des cas très particuliers de pipeline spécialement conçus pour pour trouver des dépendances WAR, et ils ne sont pas utilisés dans des processeurs commerciaux. En pratique, les dépendances WAR n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce que presque tous les processeurs avec exécution dans le désordre font. On va donc les mettre de côté dans ce qui suit.

Les dépendances WAW n'existent pas sur un pipeline de longueur fixe, sauf en cas de "défaut de conception" ou de pipeline conçu avec des limitations précises. Mais ce n'est pas le cas avec un pipeline à instruction multicycle, car si l'ordre de démarrage des instructions est respecté, l'ordre des lectures/écritures ne l'est pas. Les dépendances WAW n'apparaissent que si le pipeline autorise les instructions sur plusieurs cycles d’horloge ou les écritures qui prennent plusieurs étages.

L'exécution dans l'ordre et dans le désordre des instructions

modifier

Nous venons de voir que les dépendances de données posent des problèmes qu'un pipeline de longueur variable doit résoudre. La plupart de ces problèmes de dépendances n'existent pas sur un pipeline à longueur fixe, car ils n'ont pas de dépendances WAR et WAW. Le fait que leurs écritures dans les registres se font dans l'ordre du programme résout ce genre de dépendances. Mais les pipelines de longueur variables doivent trouver des solutions alternatives. Pour les dépendances RAW, elles sont résolues soit par des bulles de pipeline, soit par contournement, cela ne change pas avec les pipelines de longueur variable.

La solution la plus simple détecte les instructions dépendantes et retarde les instructions problématiques de quelques cycles d'horloges. Si une instruction a une dépendance problématique avec une autre, il suffit simplement de l’exécuter avec un retard de cycles. Pour cela, lorsqu'une instruction multicycle est démarrée, les instructions problématiques sont bloquées à l'étage de décodage en attendant le bon moment. Pour cela, on utilise des bulles de pipeline (pipeline bubble), pour retarder l'instruction fautive dans l'unité de décodage.

 
Bulle de pipeline.

Notons que cette technique exécute des instructions indépendantes dans des unités de calcul séparées. Dans l'exemple précédent avec une instruction multicycle suivie par une instruction normale, le processeur détecte les situations où ce n'est pas grave si la seconde instruction finisse avant la première : on peut lors lancer la seconde instruction immédiatement sans attendre. Il y a donc un gain en performance. Un point important est que cette technique émet les instructions dans l'ordre, mais qu'elles peuvent s'exécuter dans le désordre et terminer dans le désordre. En clair, les instructions sont injectées dans les unités de calcul dans l'ordre, mais en sortent dans le désordre.

Pour éviter les bulles de pipeline, les processeurs modernes incorporent des optimisations assez intéressantes. Et nous avons déjà vu une des plus intéressante : utiliser le contournement pour réduite l'usage de bulles de pipeline en cas de dépendance Read after write. Le contournement doit adapté sur les processeurs à instructions multicycle, mais il reste le même dans les grandes lignes. La différence est que la sortie de chaque unité de calcul doit être connectée aux entrées de toutes les autres. Le réseau d'interconnexion qui en découle est généralement très complexe.

Une autre solution est ce qu'on appelle l'exécution dans le désordre, une optimisation commune sur les processeurs modernes qui sera vue en détail dans plusieurs chapitres dans la suite du cours. L'idée est que si une instruction problématique est bloquée dans l'unité de décodage/émission, elle est juste mise en attente, elle ne bloque pas le pipeline. Le processeur regarde les instructions suivantes et vérifie si certaines d'entre elles peuvent s'exécuter. Les instructions mises en attente sont exécutées dès que possible, elles ont la priorité. La gestion de la mise en attente des instructions problématiques est assez complexe et implique de les stocker dans des mémoires spécifiques. Là encore, il faut que les instructions exécutées en parallèle ne manipulent pas les mêmes données, ni les mêmes registres, ni les mêmes adresses mémoires.

La toute première implémentation de l'exécution dans le désordre était sur l'ordinateur CDC 6600, suivi par son successeur le CDC 7600. Il intégrait un circuit appelé le scoreboard, qui gérait l'exécution dans le désordre. Décrire le circuit en question est assez compliqué, aussi je le laisse de côté pour le moment. Sachez le terme scoreboard est resté et a été élargit à tout circuit d'émission, qu'il soit dans le désordre ou dans l'ordre.

L'étage d'émission : la génération de bulles de pipeline

modifier

Pour éviter tout problème, il faut impérativement détecter à l'avance les dépendances de données, et agir en fonction. Pour cela, on rajoute un étage au pipeline, l'étage d'émission (issue), qui détecte les situations problématiques et rajoute des bulles de pipeline si besoin. Il suit immédiatement l'étage de décodage, et est situé avant le chemin de données. L'unité d'émission gère aussi bien les accès mémoire que les instructions multicycle, il peut aussi configurer les circuits de contournement pour les utiliser au mieux.

Le rôle de l'unité d'émission est de générer les bulles de pipeline. Si elle reçoit une instruction problématique, elle la bloque dans l'unité d'émission et génère des bulles de pipeline, tant que la dépendance n'est pas réglée. Mais si l'instruction peut s'exécuter, elle l'envoie dans la suite du pipeline. On dit qu'elle émet l'instruction. L'émission est donc l'envoi d'une micro-opération dans le pipeline, si les conditions sont réunies. Un point important de l'émission est que le processeur émet une instruction par cycle, en absence de dépendance de données, mais aussi en absence de dépendances structurelles, les deux étant détectées par l'unité d'émission.

La gestion des dépendances de données

modifier

L'unité d'émission doit détecter les dépendances de données entre instructions. Elle cherche à émettre une instruction, appelée instruction candidate. Son rôle est de détecter si l'instruction candidate a une dépendance avec une instruction déjà dans le pipeline. Les instructions dans le pipeline n'ont pas encore enregistrées leurs résultats dans les registres, soit parce qu'elles sont en cours d'exécution, qu'elles lisent leurs opérandes, peut importe. Elles seront appelées instructions "en vol". L'unité d'émission détecte les dépendances entre l'instruction candidate et chaque instruction en vol. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons.

Pour cela, il compare les registres utilisés par l'instruction à émettre et les compare avec les registres manipulés par les instructions émises dans le pipeline. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les registres opérandes sont ceux qui sont lus, ceux où sont les opérandes. Le registre de destination d'une instruction est celui dans lequel on enregistre le résultat. Les quatre situations possibles sont les suivantes :

  • Un des registres opérande est à lire (opérande) par une instruction dans le pipeline : pas de dépendance.
  • Un des registres opérande est écrit (destination) par une instruction dans le pipeline : dépendance RAW.
  • Un des registres de destination est à lire (opérande) par une instruction dans le pipeline : potentielle dépendance WAR, mais seulement sur des pipelines pathologiques jamais utilisés.
  • Un des registres de destination est écrit (destination) par une instruction dans le pipeline : dépendance WAW.

Notez que pour les lectures, on part du principe que les instructions en vol n'ont pas encore lu leurs registres opérandes. Dès qu'elles ont lu les opérandes en question, les dépendances liées disparaissent. Pareil pour les écritures, sauf que celles-ci ont lieu à la toute fin du pipeline. Les écritures en attente ont donc lieu quand l'instruction termine.

  • Instruction candidate
  • Instructions en vol
Registres opérandes Registre destination
Registres pas encore lus (opérandes) Pas de dépendances (dépendance RAR inoffensive) Dépendance WAR potentielle, pas en pratique
Registre pas encore écrits (destination) Dépendance RAW Dépendance WAW

L'algorithme est donc le suivant : une lecture est autorisée en absence d'écriture (dans le même registre), une écriture est autorisée en absence de lecture (dans le même registre). Cependant, on peut grandement simplifier l'algorithme et ne pas tenir compte des dépendances WAR, qui n'existent pas sur la plupart des pipelines dynamiques sans exécution dans le désordre. Le tableau se simplifie en retirant les dépendances inutiles, seule la dernière ligne nous intéresse au final.

  • Instruction candidate
  • Instructions en vol
Registres opérandes Registre destination
Registre pas encore écrits (destination) Dépendance RAW Dépendance WAW

L'unité d'émission doit connaitre les registres de destination des instructions en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut sortir une bulle de pipeline ou non.

 
Détection des dépendances dans un pipeline.

Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple registre, appelé le registre de disponibilité. Le registre de disponibilité permet à une instruction émise de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être écrit par une autre instruction. Les registres qui ne sont pas réservés sont dit libres ou disponibles. Si une instruction candidate veut lire ce registre, c'est signe qu'il y a une dépendance RAW : elle veut lire un registre réservé, qui est destiné à être écrit par une instruction en vol. Si elle veut écrire, c'est signe qu'elle veut écrire dans un registre où une autre instruction veut écrire : c'est une dépendance WAW. Dans les deux cas, l'instruction n'est pas émise.

Le registre encode les registres réservés comme suit : chaque bit est associé à un registre. Le bit numéro 0 est associé au registre numéro 0, le bit numéro 1 au registre numéro 1, etc. Là, il existe deux versions du registre de disponibilité. La première version indique qu'un registre est réservé en mettant le bit associé à 1. Une autre version utilise un bit à 0. La différence est la suivante, en supposant que un bit à 1 : la première indique que le registre est réservé, l'autre qu'il est libre. Dans la suite, on suppose qu'il est à 1 pour un registre réservé.

Vous aurez remarqué que ce registre de disponibilité ressemble beaucoup au registre de disponibilité des pipelines fixes. Mais il y a une grosse différence. Sur un processeur à pipeline fixe, il n'y a pas de dépendances WAW/WAR. Ce qui fait que le processeur ne vérifie que les registres opérandes, pas le registre de destination. Mais sur un pipeline dynamique, le processeur doit aussi comparer le registre de destination avec le registre de disponibilité.

Avant d'émettre une instruction, l'unité d'émission extrait les registres opérande/destination et consulte le registre de disponibilité. Si l'émission est autorisée, le registres de disponibilité est là aussi mis à jour. Le registre de disponibilité est aussi mis à jour dès qu'une instruction enregistre son résultat dans les registres, ou alors dès qu'il est disponible pour le contournement. Le bit associé au registre repasse alors à 0 (ou à 1). Sans contournement, la mise à jour se fait alors à la toute fin de l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul.

L'implémentation en portes logiques est très simple. En premier lieu, les noms de registres opérande/destination sont envoyés dans un décodeur chacun. Le résultat en sortie est un nombre en représentation unaire/one-hot qui indique quel registre est lu/écrit. Les résultats passent dans un OU bit à bit, pour former un masque qui indique quels registres sont lus/écrits. Le masque est alors comparé avec le registre de disponibilité avec un ET bit à bit. Le résultat dit si l'émission est autorisée ou non. Notons que le fait d'utiliser un bit de disponibilité à 1/0 pour indiquer un registre réservé change le circuit de comparaison.

Passons maintenant à la mise à jour du registre de disponibilité à l'émission. Son implémentation est simple : on reprendre le résultat en sortie du décodeur, pour le registre de disponibilité. Il est alors combiné avec le registre de disponibilité, en faisant un simple OU logique.

 
Unité d'émission simple, dans l'ordre

Vous aurez peut-être remarqué, mais une différence est que les pipelines fixes retardent les écritures dans le banc de registre avec des registres d'échelonnage, les pipelines dynamique retardent l'émission des instructions. Les deux méthodes ont sensiblement le même résultat.

Le registre de consultation permet de désigner les registres qui vont être lus par les instructions en vol. Je précise bien : les registres qui n'ont pas encore été lus. Les registres en question sont dit consulté, les autres sont dit non-consultés. Il est interdit d'écrire dans un registre consulté, c'est signe d'une dépendance WAR. Il faut noter que beaucoup de pipeline s'en passent car ils n'ont pas de dépendances WAR. Le registre de consultation est mis à jour dès que l'opérande est lue. Et cela arrive assez tôt dans le pipeline, au bout de quelques cycles grand maximum. Sur beaucoup de pipelines, il n'est pas présent dans il est impossible qu'une écriture ait lieu avant qu'une lecture ait démarrée. Mais sur les pipelines où de telles dépendances sont possibles, il faut en tenir compte.

Il existe quelques rares processeurs qui fusionnent le registre de disponibilité et de consultation en un seul. Le registre contient autant de bits que de registres, chaque bit indique si le registre associé est pas encore lu/écrit. De tels implémentations détectent alors toutes les dépendances, même les dépendances Read After Read. Elles tendent à être très conservatrices. Un exemple est le co-processeur vectoriel VFP9-S d'ARM. Leur implémentation est simple, mais il peut y avoir un cout en performance.

La gestion de la disponibilité des ALUs

modifier

L'étage d'émission détecte aussi les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Si un tel conflit potentiel est détecté, il met en attente l'instruction tant qu'un conflit est possible. Reste à détecter les conflits d'accès, les dépendances structurelles.

Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit insérer des bulles de pipeline.

Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le vecteur de disponibilité. Le bit est mit à 1 lorsque l'instruction est émise, il est remis à 0 quand l'unité de calcul est libre. Avant d'émettre une instruction, le scoreboard vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Une simple opération de masquage suffit entre ce masque et le vecteur de disponibilité. Si le résultat est différent de 0, l'instruction n'a pas le droit d'être émise.

Toute la difficulté tient dans la remise à 0 des bits de disponibilité. Une première solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. Une autre solution, plus pratique, intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0.

La gestion des accès mémoire

modifier

Il faut noter que l'unité d'émission prend aussi en charge la gestion des accès mémoire, encore plus sur les pipelines dynamiques. Les accès mémoire ont pour défaut qu'ils ont un nombre de cycles variable. Sur la plupart des processeurs, l'accès au cache se fait rapidement, mais l'accès à la mémoire est très lent. Pour éviter tout problème, les instructions qui suivent l'accès mémoire sont donc mises en attente tant qu'il n'est pas terminé.. Pour cela, dès que l'unité d'émission émet une micro-opération mémoire, elle émet des bulles de pipeline à sa suite. Une fois l'accès mémoire terminé, l'unité d'accès mémoire envoie un signal READ_FINISH ou WRITE_FINISH qui prévient l'unité d'émission. L'émission des instructions reprend.

D'autres techniques plus élaborées permettent de ne pas geler le pipeline, de ne pas émettre de bulles de pipeline. L'idée est de continuer à faire des calculs en parallèle de l'accès mémoire, sous condition que cela ne pose pas de problèmes. L'idée de base est assez simple : si l'unité d'émission rencontre une lecture, il continue d’émettre des instructions, mais seulement si elles sont indépendantes de la lecture. Les instructions indépendantes d'une lecture sont émises pendant que celle-ci attend la mémoire ou le cache. Cette technique s'appelle les lectures non-bloquantes. Elle a été utilisée sur le processeur ARM Cortex A53 et sur son successeur, l'ARM Cortex A510, avec un certain succès.

L'implémentation est assez simple. L'unité d'émission a juste à mémoriser le registre de destination de la lecture, celui dans lequel sont rangées la donnée lue. Il suffit de marquer ce registre comme utilisé, réservé, et de bloquer l'émission de toute instruction qui l'utilise. Mais les instructions qui ne lisent ou n'écrivent pas ce registre peuvent s'exécuter. Les lectures non-bloquantes peuvent être vues comme une sorte d'exécution dans le désordre limitée aux lectures.