Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans l'ordre

Dans ce chapitre, nous allons voir comment fonctionne l'unité mémoire d'un processeur avec un pipeline. Du point de vue du pipeline, une unité mémoire avec calcul d'adresse intégré se comporte comme n'importe quelle unité de calcul : elle prend en entrée des opérandes et fournit un résultat en sortie, ce qu'il y a à l'intérieur n'a pas d'importance du point de vue de l'unité d'émission ou des autres circuits. Les opérandes envoyées en entrée sont les adresses à lire/écrire, et la donnée à écrire pour une écriture. La sortie n'est utilisée que pour les lectures, elle présente la donnée lue.

Cependant, il y a quelques points qui font que l'unité mémoire est un peu à part. Premièrement, les accès mémoire ont un temps d'exécution variable. Un accès au cache peut prendre deux à trois cycles d’horloge, un accès au cache L2 prend facilement 10 cycles, et un accès en RAM une centaine. Les latences sont pas très compatibles avec un pipeline, car elles sont très variables. De plus, la latence d'un défaut de caches est souvent supérieure à la longueur du pipeline. Gérer les accès mémoire est donc assez compliqué.

Vous vous dites sans doute que les accès mémoire sont des instructions multicycles, et ce n'est pas faux. Mais vu que leur latence n'est pas connue à l'avance, les techniques utilisées sur les pipelines multicycle/dynamiques ne fonctionnent pas parfaitement. La gestion des dépendances de données est notamment totalement différente. Dans ce chapitre, nous allons voir une unité mémoire qui ne peut pas gérer plusieurs accès mémoire simultanés. Pour le dire autrement, quand l'unité mémoire est en train d'effectuer un accès mémoire, elle ne peut pas accepter un nouvel accès mémoire. Nous les appellerons les unités mémoire bloquantes. Bloquante, sous-entendu qu'elles sont bloquées pendant qu'elles font un accès mémoire.

L'unité mémoire : la Load-Store Unit

modifier

Les micro-opérations mémoires sont gérées par un circuit spécialisé, appelé l'unité mémoire, ou encore la Load-Store Unit. Nous utiliserons le terme unité mémoire dans la suite du cours. En soi, nous avons déjà vu cette unité, dans le chapitre sur le chemin de données, dans la section de quatre chapitres portant sur la microarchitecture d'un processeur. Nous avions parlé de l'unité de communication avec la mémoire, sous-entendu la mémoire RAM. Mais sur un pipeline, il y a quelques petites différences avec l'unité précédente.

La première différence est que l'unité mémoire doit faire avec la présence d'un cache de données. Elle s'interface avec le cache, pas avec la mémoire RAM. De plus, gérer une hiérarchie de cache demande quelques modifications. La seconde différence tient dans la manière dont sont calculées les adresses, qui force à intégrer l'unité mémoire différemment dans le processeur. Avec un pipeline, il n'y a pas pas de bus de communication qui relie entre eux unités de calcul, registres et unité mémoire. A la place, l'unité mémoire est reliée aux registres et aux réseau de contournement. Suivant comment s'effectue le calcul d'adresse, le réseau de contournement est connecté d'une manière ou d'une autre à l'unité mémoire. Notamment, les registres d’interfaçage mémoire disparaissent et sont remplacés par des registres de pipeline reliés au réseau de contournement.

L'interface de l'unité mémoire

modifier

L'unité mémoire est généralement multiport, avec un port d'entrée et un port de sortie, afin de faciliter son insertion dans le pipeline.

  • Le port d'entrée est là où on envoie l'adresse à lire/écrire, ainsi que la donnée à écrire pour les écritures. L'adresse peut provenir du décodeur pour l'adressage absolu, des registres pour les modes d'adressage indirects. Pour les modes d'adressages à calcul d'adresse, les choses sont un peu compliquées, mais on va dire qu'ils proviennent des unités de calcul.
  • Le port de sortie est utilisé pour récupérer le résultat des lectures. Il faut aussi tenir compte du fait que les données lues sont envoyées aux registres, éventuellement aux ALU via le réseau de contournement. Pour cela, le port de sortie fournit le registre de destination afin de configurer correctement le banc de registre et le réseau de contournement.

Une unité mémoire est donc reliée au chemin de données via deux entrées et une sortie : une entrée d'adresse, une entrée de données, et une sortie pour les données lues. La sortie est connectée aux registres, via le port d'écriture du banc de registres, et éventuellement au réseau de contournement des ALU.

 
Unité d'accès mémoire LOAD-STORE.

L'unité mémoire a généralement deux ports : une pour les micro-opérations de lecture, et un pour les micro-opérations d'écriture. Le port d'écriture contient deux entrées : une pour l'adresse à écrire, et une autre pour la donnée à écrire. Le port de lecture n'a qu'une seule entrée pour l'adresse à lire. Les deux entrées d'adresse sont directement reliées aux unités de calcul d'adresse intégrées dans l'unité mémoire.

Les calculs d'adresse et l'unité mémoire

modifier

Les micro-opérations mémoire ont besoin d'une adresse pour faire une lecture/écriture. En général, l'adresse est lue dans les registres, mais elle peut aussi être fournie par le réseau de contournement. Mais certains modes d'adressage font que l'adresse doit être calculée. L'adressage base + indice est un bon exemple : il demande de calculer l'adresse à lire/écrire avec une addition et éventuellement un décalage. Les calculs d'adresse peuvent être effectués soit dans les unités de calcul entières, soit dans des unité de calcul d'adresse séparées, soit dans l'unité mémoire elle-même.

La première solution effectue les calculs d'adresse dans une unité mémoire séparée, totalement autonome et séparée du reste du pipeline. Les deux autres solutions utilisent des ALU séparées. Les deux cas se distinguent sur un point d'implémentation : est-ce qu'il y a un aval séparé pour les accès mémoire ? Si l'unité mémoire effectue les calculs d'adresse, alors un a un aval séparé pour les micro-opérations mémoire, qui se comporte comme une unité de calcul. Il ne communique avec le reste du pipeline que via les registres et le réseau de contournement. Mais si une ALU externe fait les calculs d'adresse, alors ce n'est pas le cas. Il y a un aval qui s'occupe à la fois des micro-opérations entières et des micro-opérations mémoire.

La conséquence est que les instructions mémoire sont décodées en une ou deux micro-opérations. Si l'unité mémoire fait les calculs d'adresse, une instruction mémoire n'utilise qu'une seule micro-opération exécutée par l'unité d'accès mémoire, qui se charge de la mettre en attente et de l'exécuter dès que possible. Si le calcul d'adresse est réalisé par une ALU externe, une instruction mémoire est décodée en deux micro-opérations : une micro-opération de calcul d'adresse et une une micro-opération d'accès mémoire. La première micro-opération s'exécute dans une ALU, et son résultat est envoyé à l'unité mémoire, qui fera le lien entre adresse calculée et micro-opération mémoire.

La solution la plus simple est celle qui effectue les calculs d'adresse dans l'unité mémoire elle-même. Celle-ci incorpore des additionneurs et éventuellement un circuit de décalage pour faire les calculs d'adresse. Les circuits de calcul d'adresse sont alors redondants entre l'ALU entière et l'unité mémoire, ce qui a un léger cout en circuit. Mais un accès mémoire est garantit de se faire en une seule micro-opération, ce qui simplifie le décodage. L'interface de l'unité mémoire doit aussi être modifiée, de manière à pouvoir lui envoyer les indices et décalages nécessaires au calcul d'adresse.

Du point de vue du pipeline, une unité mémoire avec calcul d'adresse intégré se comporte comme n'importe quelle unité de calcul : elle prend en entrée des opérandes et fournit un résultat en sortie, ce qu'il y a à l'intérieur n'a pas d'importance du point de vue de l'unité d'émission ou des autres circuits. Les opérandes envoyées en entrée sont les adresses à lire/écrire, et la donnée à écrire pour une écriture. La sortie n'est utilisée que pour les lectures, elle présente la donnée lue.

Si une ALU externe calcule l'adresse, l'adresse calculée est envoyée à l'unité mémoire via un système de contournement dédié, qui relie la sortie de ces unités de calcul à l'unité mémoire, sur les ports d'adresse. L'unité mémoire doit déterminer à quelle accès mémoire l'adresse calculée correspond. Cette mise en correspondance est gérée par des mécanismes qu'on verra plus bas.

Le choix entre ALU entière ou ALU de calcul d'adresse est un compromis entre performance et cout en interconnexions. Utiliser l'ALU entière est moins couteux en transistors, pas besoin d'ajouter des circuits de calcul d'adresse. Par contre, utiliser des unités de calcul d'adresse séparées simplifie drastiquement le réseau de contournement. L'unité de calcul d'adresse a sa sortie reliée uniquement à l'unité mémoire, alors qu'une ALU entière est déjà intégrée dans un réseau de contournement complexe. Rajouter une connexion entre ALU entière et unité mémoire n'est pas trivial, et a un cout en termes de performance.

L'intérieur d'une unité mémoire

modifier

L'intérieur de l'unité mémoire n'est pas très complexe. Elle contient parfois des unités de calcul d'adresse pour gérer les modes d'adressage base + indice et autres. Et puis, elle contient le cache de données et tout ce qui le relie au reste de la hiérarchie de cache. L'unité mémoire s'occupe de l'accès au cache L1, mais délègue les défauts de cache aux circuits du cache. Elle détecte les succès ou défaut de cache et communique avec le reste du pipeline pour gérer les défauts de cache.

Le pipelining des micro-opérations mémoire

modifier

Les unités mémoire évoluées sont capables de gérer plusieurs accès mémoire simultanés. La solution la plus simple pour cela met en attente les micro-opérations mémoire dans l'unité mémoire, et les exécute une par une. On peut alors envoyer plusieurs accès mémoire simultanés du point de vue de l'unité d'émission, mais qui sont exécutés en série par l'unité mémoire. Une solution assez simple, qui évite de bloquer le pipeline quand deux accès mémoire simultanés ont lieu. Mais faire ainsi pose de nombreux problèmes, car ils faut mettre en attente les accès mémoire, gérer le calcul des adresses, et ce genre de choses. Aussi, il faut aussi gérer les dépendances mémoire.

Une autre solution consiste à pipeliner l'accès à la mémoire. La méthode la plus simple pipeline l'accès au cache, l'autre pipeline l'accès à la mémoire. Pipeliner la mémoire est une méthode assez ancienne, utilisée sur les anciens ordinateurs historiques, qui ne disposaient pas de cache, mais avaient bien un pipeline (et parfois, de l'exécution dans le désordre et du renommage de registres). Typiquement, pipeliner l'accès à la mémoire est assez simple : il suffit d'utiliser des mémoires entrelacées. Pipeliner l'accès au cache est une technique en vigueur dans les processeur modernes, même ceux avec un pipeline dynamique. Voyons là plus en détail.

Beaucoup de processeurs des années 2000 avaient une fréquence peu élevée comparé aux standard d'aujourd'hui, ce qui fait que leur cache avait bien un temps d'accès d'un cycle d'horloge. Mais ils étaient quand même capables de pipeliner les accès mémoire. L'accès au cache se faisait en deux cycles d'horloge, avec un cycle pour calculer une adresse et un cycle pour l'accès mémoire proprement dit. C'était le cas sur les processeurs AMD d'architecture K5 et K6, sur les processeurs Intel d'architecture P6 (Pentium 2 et 3) et quelques autres.

L'avantage de faire ainsi est que l'on peut pipeliner les micro-opérations mémoire alors que l'accès au cache se fait en un cycle d'horloge. Deux micro-opérations mémoire peuvent s'exécuter en même temps sans trop de problèmes. Un autre avantage est que l'on peut réutiliser les unités de calcul normale pour faire les calculs d'adresse. Les processeurs AMD Athlon utilisaient cette technique : les unités de calcul étaient soit utilisées comme unité de calcul entières, soit pour calculer des adresses.

La technique marche aussi bien sur les pipeline dynamiques que multicycle. Avec un pipeline dynamique, Les micro-opérations entières sont alors plus courtes d'un cycle que les micro-opérations de lecture/écriture. Mais avec un pipeline multicycle, on se retrouve avec une organisation assez proche du pipeline RISC classique, avec deux étages EXEC et MEM pour chaque instruction.

L'exécution en parallèle des micro-opérations mémoire

modifier

Passons maintenant à la manière dont pipelines dynamiques, de longueur variable, gèrent les accès mémoire. Les accès mémoire ont une latence qui est inconnue, ce qui pose de nombreux problèmes, notamment pour détecter les dépendances de données. Pour éviter tout problème, l'idéal est de stopper l'exécution d'autres instructions tant qu'un accès mémoire a lieu. Quand une micro-opération d'accès mémoire est émise, le processeur cesse d'émettre des instructions, il émet juste des bulles de pipeline. C'est la solution la plus simple, mais elle est imparfaite.

Une optimisation profite de l'unité de calcul en parallèle de l'unité d'accès mémoire. 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 le processeur rencontre une lecture, il continue d’exécuter des instructions malgré tout, en parallèle de la lecture, si elles sont indépendantes de la lecture. Les instructions indépendantes d'une lecture s’exécutent 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.

Les lectures non-bloquante peuvent être vues comme une sorte d'exécution dans le désordre limitée aux lectures. La différence avec l'exécution dans le désordre est que la technique des lectures non-bloquantes détecte si une instruction est dépendante d'une lecture, alors que l’exécution dans le désordre détectent toutes les dépendances entre instructions, pas seulement les dépendances avec un accès mémoire.

Mais faire ainsi fait naitre pas mal de problèmes liés aux dépendances de données. Le premier problème est qu'il se peut qu'une instruction ultérieure utilise la donnée lue par l'accès mémoire. Par exemple, la lecture charge une donnée dans un registre et ce registre est ensuite utilisé comme opérande d'une addition. Dans ce cas, l'instruction de calcul ne doit pas s’exécuter tant que la donnée n'a pas été chargée. Un autre cas est celui où une instruction écrit dans le registre de destination de la lecture. Sans lectures non-bloquantes, la lecture a lieu avant, ce qui fait que la lecture écrit dans ce registre, qui est ensuite modifié par l'instruction ultérieure. Mais avec des lectures non-bloquantes, l'instruction qui écrit dans ce registre ne doit pas démarrer tant que la lecture n'est pas terminée.

L'implémentation de cette technique est assez simple, et se fait soit dans l'unité de décodage, soit dans l'unité d'émission. Les deux sont possibles mais nous allons partir sur l'unité d'émission. Dans les deux cas précédents, on remarque que les problèmes ont lieu quand une instruction ultérieure veut lire/écrire le registre de destination de la lecture, à savoir le registre où on charge la donnée lue. Pour détecter les instructions dépendantes d'une lecture, l'unité d'émission marque le registre de destination de la lecture comme « invalide », du moins tant que la lecture n'a pas écrit son résultat dedans. Pour marquer les registres comme valides ou invalides, on utilise un registre dont chaque bit est associé à un registre. Chaque bit indique si le registre associé contient une donnée valide ou non. Le bit de validité est automatiquement comme invalide quand le processeur démarre une lecture.

Toute instruction qui a un registre invalide comme opérande est mise en attente. idem pour les instructions qui ont un registre invalide comme registre de destination. A chaque cycle, l'unité d'émission tente de démarrer une nouvelle instruction et vérifie si elle est dépendante de la lecture. Elle vérifie si l'instruction à émettre a le registre de destination de la lecture comme opérande, ou comme registre de destination. Si c'est le cas, alors l'instruction est bloquée dans le pipeline. Mais dans le cas contraire, l'instruction de calcul est indépendante de l'accès mémoire, et peuvent démarrer sans trop de problèmes.

Le processeur ROCK, annulé en 2010, utilisait une version améliorée de la technique précédente, appelée la technique des éclaireurs matériels. La différence est que le processeur ne stoppe pas quand il rencontre une instruction dépendante de la lecture. A la place, les instructions dépendantes de la lecture sont mises en attente dans une file d'attente spécialisée, appelée la file d’attente différée (deferred queue), qui est une mémoire FIFO un peu modifiée. Une fois que la lecture renvoie un résultat, le processeur cesse l’exécution des instructions et exécute les instructions mises en attente. Les instructions mises en attente sont exécutées dans l'ordre, avant de reprendre là où le programme s'était arrêté.

L'émission anticipée spéculative : une optimisation du contournement des accès mémoire

modifier

Les pipelines doivent gérer le fait que les accès mémoire ont une durée variable, souvent imprévisible. Et cela pose des problèmes pour la gestion du contournement, pour les lectures. 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 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.

En théorie, le processeur doit attendre que les lectures soient terminées pour exécuter les instructions dépendantes. L'unité d'émission doit mettre en attente les instructions dépendantes d'une lecture, comme vu plus haut. Les instructions dépendantes ne sont émises qu'une fois que l'unité mémoire a prévenu l'unité d'émission que la donnée est disponible. Le problème est qu'il y a plusieurs cycles d'horloge entre l'unité d'émission et l'entrée de l'ALU. En conséquence, entre le moment où l'instruction dépendante sera émise et celui où il est exécuté, il se passera quelques cycles durant lesquels le processeur émettra des bulles de pipeline avant que le contournement ne fasse sont œuvre.

Nous avions vu un problème similaire dans le chapitre sur l'exécution dans le désordre, dans la section "La logique d'éveil : l'émission en avance". Le problème n'impliquait pas les accès mémoire, mais seulement les instructions de calcul, mais la logique est la même : les cycles entre émission et exécution posent des problèmes pour le contournement. Dans ce précédent chapitre, nous avions parlé des techniques dites d'émission anticipée, qui émettent des instructions quelques cycles en avance pour arriver à l'ALU pile au moment où leur opérande est disponible via le réseau de contournement. Elles peuvent s'appliquer aux accès mémoire, mais avec des modifications assez importantes. Les techniques d'émission anticipée deviennent spéculatives et impliquent des systèmes de prédiction ou de replay, que nous allons voir dans ce qui suit.

L'émission anticipée part du principe que l'accès mémoire va accéder au cache, qu'il n'y aura pas de défaut de cache. 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 l'accès mémoire entraine un succès de cache, alors la donnée lue est disponible pour le contournement après 3 cycles, 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 corriger la situation demande deux choses : annuler les instructions émises à tord, et faire en sorte qu'elles soient ré-exécutées normalement. Les deux problèmes sont liés, mais ils sont à l'origine de contraintes différentes.

L'annulation des instructions émises à tort

modifier

Pour annuler les instructions émises à tort, il y a plusieurs solutions. La solution la plus simple n'annule pas les instructions émises à tord, mais gèle l'instruction dépendante sur place. 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.

Une autre solution annule toutes les instructions après celles émise à tort. Elle gère les défauts de cache avec les circuits pour les exceptions précises. L'idée est qu'un défaut de cache est géré comme une exception, mais qui ne quitterait pas le processeur. L'instruction mémoire déclenche un défaut de cache est traitée comme une exception, à savoir que les instructions mémoire qui la suivent sont invalidées. Le processeur est alors mis en mode exception, où il ne charge rien, en attendant que le défaut de cache soit terminé. Une fois le défaut de cache traité, l'instruction mémoire redémarre, mais elle finira sur un succès de cache lors de cette seconde tentative.

Un autre solution détecte quelles instructions ont été émises à tord et annule celles-ci. L'avantage est que seules les instructions lecture-dépendantes sont annulées, les autres instructions s'exécutent normalement. L'annulation sélective permet de conserver de bonnes performances en cas d'émission anticipée fautive, vu que seules les instructions émises à tort sont annulées, pas celles exécutées sans émission anticipée.

La ré-exécution des instructions émises à tort

modifier

Maintenant, voyons le second problème : ré-exécuter les instructions lecture-dépendantes. Pour ré-exécuter l'instruction lecture-dépendante, il faut que celle-ci ait été conservée quelque part. Si l'instruction a vraiment été émise en avance, elle a quitté la fenêtre d'instruction, elle disparait une fois qu'on l'a a annulée. Impossible de la ré-exécuter dans ces conditions. De plus, même si on a conservé l'instruction quelque part, l'émettre une nouvelle fois demande de l'insérer dans la fenêtre d’instruction pour la ré-émettre, et il n'est pas garantit qu'il y ait assez de place dedans à ce moment là !

La solution pour cela est de ne pas retirer les instructions lecture-dépendantes de la fenêtre d'instruction. Elles sont émises, mais une copie de sauvegarde est conservée dans la fenêtre d'instruction. Si le processeur détecte un défaut de cache, il peut alors ré-exécuter l'instruction. S'il détecte un succès de cache, les instructions lecture-dépendante sont retirées de la fenêtre d'instruction, la copie de sauvegarde est devenue inutile et est donc jetée.

Une autre solution bien plus agressive, a été utilisée sur le processeur Pentium4. La solution est appelée un pipeline à répétition (replay pipeline). L'idée 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 juste à propager 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.

La prédiction de latence mémoire

modifier

Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. L'instruction émise à tort a utilisé des ressources de pipeline qui auraient pu être utilisées à meilleur escient. Par exemple, elle a lu ses registres, si le processeur est de type "lecture après émission". La ré-éexécution est une mauvais utilisation de l'ALU, la première exécution prend potentiellement la place d'une autre instruction. Et même si le processeur n'a pas eu à lire les registres et que l'ALU était totalement libre, exécuter une instruction pour rien consomme de l'énergie et de l'électricité. Aussi, il vaut mieux réduire les émissions fautives au maximum.

Les techniques de prédiction de latence mémoire tentent de réduire les émission 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.