Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.45.0-wmf.7
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Fonctionnement d'un ordinateur/Les processeurs superscalaires
0
65956
745594
745590
2025-06-29T14:59:57Z
Mewtow
31375
/* L'interaction avec les fenêtres d'instruction */
745594
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Fivestagespipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur non-superscalaire.]]
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des avals et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Lire des instructions sur un processeur superscalaire peut paraitre simple. La méthode la plus simple consiste à doubler, tripler ou quadrupler la taille du bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... fois plus d'instruction en même temps. Sur un processeur avec des instructions de longueur fixe, la méthode marche très bien. Mais sur les CPU CISC, comme les CPU x86 des PC modernes, c'est une autre paire de manches, car on doit gérer des instruction de taille variable.
Gérer des instructions de taille variable en chargeant plusieurs instructions à la fois est un vrai défi. Il faut découper le bloc chargé en plusieurs instructions, délimiter les frontières de chaque instruction dans le bloc. Le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement est donc fortement modifié. Sur le principe, il est dupliqué : on détecte la première instruction, décale le résultat pour récupérer le reste du bloc, puis on recommence tant que le bloc ne contient plus d'instructions. Le résultat est que cela prend parfois un étage de pipeline entier, c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté, et passons au vrai problème du chargement des instructions sur un CPU superscalaire. Charger un gros bloc de mémoire permet de charger plus d'instructions à la fois, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention'''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
5jp05a8ognn2446q0189mv2prnz7wru
745595
745594
2025-06-29T15:00:08Z
Mewtow
31375
/* Résumé */
745595
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Fivestagespipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur non-superscalaire.]]
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des avals et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Lire des instructions sur un processeur superscalaire peut paraitre simple. La méthode la plus simple consiste à doubler, tripler ou quadrupler la taille du bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... fois plus d'instruction en même temps. Sur un processeur avec des instructions de longueur fixe, la méthode marche très bien. Mais sur les CPU CISC, comme les CPU x86 des PC modernes, c'est une autre paire de manches, car on doit gérer des instruction de taille variable.
Gérer des instructions de taille variable en chargeant plusieurs instructions à la fois est un vrai défi. Il faut découper le bloc chargé en plusieurs instructions, délimiter les frontières de chaque instruction dans le bloc. Le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement est donc fortement modifié. Sur le principe, il est dupliqué : on détecte la première instruction, décale le résultat pour récupérer le reste du bloc, puis on recommence tant que le bloc ne contient plus d'instructions. Le résultat est que cela prend parfois un étage de pipeline entier, c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté, et passons au vrai problème du chargement des instructions sur un CPU superscalaire. Charger un gros bloc de mémoire permet de charger plus d'instructions à la fois, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
277twjnmvm9lmd33lqsxm7qs684hjao
745596
745595
2025-06-29T15:24:03Z
Mewtow
31375
/* La double émission entière-flottante */
745596
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Fivestagespipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur non-superscalaire.]]
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des avals et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Lire des instructions sur un processeur superscalaire peut paraitre simple. La méthode la plus simple consiste à doubler, tripler ou quadrupler la taille du bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... fois plus d'instruction en même temps. Sur un processeur avec des instructions de longueur fixe, la méthode marche très bien. Mais sur les CPU CISC, comme les CPU x86 des PC modernes, c'est une autre paire de manches, car on doit gérer des instruction de taille variable.
Gérer des instructions de taille variable en chargeant plusieurs instructions à la fois est un vrai défi. Il faut découper le bloc chargé en plusieurs instructions, délimiter les frontières de chaque instruction dans le bloc. Le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement est donc fortement modifié. Sur le principe, il est dupliqué : on détecte la première instruction, décale le résultat pour récupérer le reste du bloc, puis on recommence tant que le bloc ne contient plus d'instructions. Le résultat est que cela prend parfois un étage de pipeline entier, c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté, et passons au vrai problème du chargement des instructions sur un CPU superscalaire. Charger un gros bloc de mémoire permet de charger plus d'instructions à la fois, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
s3mpqf2q70d37hm3rlpp4vvy5sb5b52
745597
745596
2025-06-29T15:26:34Z
Mewtow
31375
745597
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des avals et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Lire des instructions sur un processeur superscalaire peut paraitre simple. La méthode la plus simple consiste à doubler, tripler ou quadrupler la taille du bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... fois plus d'instruction en même temps. Sur un processeur avec des instructions de longueur fixe, la méthode marche très bien. Mais sur les CPU CISC, comme les CPU x86 des PC modernes, c'est une autre paire de manches, car on doit gérer des instruction de taille variable.
Gérer des instructions de taille variable en chargeant plusieurs instructions à la fois est un vrai défi. Il faut découper le bloc chargé en plusieurs instructions, délimiter les frontières de chaque instruction dans le bloc. Le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement est donc fortement modifié. Sur le principe, il est dupliqué : on détecte la première instruction, décale le résultat pour récupérer le reste du bloc, puis on recommence tant que le bloc ne contient plus d'instructions. Le résultat est que cela prend parfois un étage de pipeline entier, c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté, et passons au vrai problème du chargement des instructions sur un CPU superscalaire. Charger un gros bloc de mémoire permet de charger plus d'instructions à la fois, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
e11yqhu6oifth6f8adeh3isf54l4l5c
745598
745597
2025-06-29T15:27:40Z
Mewtow
31375
/* La duplication des avals et les contraintes d’appariement */
745598
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Lire des instructions sur un processeur superscalaire peut paraitre simple. La méthode la plus simple consiste à doubler, tripler ou quadrupler la taille du bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... fois plus d'instruction en même temps. Sur un processeur avec des instructions de longueur fixe, la méthode marche très bien. Mais sur les CPU CISC, comme les CPU x86 des PC modernes, c'est une autre paire de manches, car on doit gérer des instruction de taille variable.
Gérer des instructions de taille variable en chargeant plusieurs instructions à la fois est un vrai défi. Il faut découper le bloc chargé en plusieurs instructions, délimiter les frontières de chaque instruction dans le bloc. Le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement est donc fortement modifié. Sur le principe, il est dupliqué : on détecte la première instruction, décale le résultat pour récupérer le reste du bloc, puis on recommence tant que le bloc ne contient plus d'instructions. Le résultat est que cela prend parfois un étage de pipeline entier, c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté, et passons au vrai problème du chargement des instructions sur un CPU superscalaire. Charger un gros bloc de mémoire permet de charger plus d'instructions à la fois, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
ayu7bdpjzitiect44sjj5tcaspbi0ta
745599
745598
2025-06-29T15:31:24Z
Mewtow
31375
/* L'étape de chargement superscalaire */
745599
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Le premier problème que rencontre une architecture superscalaire est de charger plusieurs instructions en même temps. La méthode la plus simple double, triple ou quadruple le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Sur un processeur avec des instructions de longueur fixe, la méthode marche très bien. Mais sur les CPU CISC avec des instructions de taille variable, des problèmes surviennent.
Charger plusieurs instructions de taille variable en même temps est un vrai défi. Un bloc de plusieurs octets est chargé depuis le cache et il faut le découper en instructions, délimiter les frontières des instructions dans le bloc. Le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement est donc fortement modifié. Sur le principe, il est dupliqué : on détecte la première instruction, décale le résultat pour récupérer le reste du bloc, puis on recommence tant que le bloc ne contient plus d'instructions. Le résultat est que cela prend parfois un étage de pipeline entier, c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
5j1bzcf6imjlv9d4fnmjpz4yenmf5l5
745600
745599
2025-06-29T15:40:01Z
Mewtow
31375
/* L'étape de chargement superscalaire */
745600
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
0fsgaq4qceu4asfkfuckazyaozstejo
745601
745600
2025-06-29T15:40:50Z
Mewtow
31375
/* Le circuit de fusion de blocs */
745601
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement.
Mais d'autres sont plus malins et partent du principe que le branchement est pris : ils exécutent toutes les instructions d'un bloc, sauf celles qui suivent un branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
D'autres chargent les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
851e2h5da6uhmga8d339typi6j6nr9v
745602
745601
2025-06-29T15:43:51Z
Mewtow
31375
/* Le circuit de fusion de blocs */
745602
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Mais il reste à déterminer si une trace peut être réutilisée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement utilisées pour construire la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
k57fgh4a5f5u2ywdphsaopcyr1c6yjm
745603
745602
2025-06-29T15:47:39Z
Mewtow
31375
/* Le cache de traces */
745603
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces.
Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces demande d'utiliser une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. Malheureusement, ces dernières sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
p9jqd64hige9nd8qlw4k4ahrua06s3q
745604
745603
2025-06-29T15:58:43Z
Mewtow
31375
/* Le cache de traces */
745604
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D.
Un cache de trace idéal peut mémoriser les deux traces ABC et ABD. Pour le dire autrement, il peut mémoriser des traces différentes, même si leur début est le même. Il fera la différence entre les traces en regardant les branchements pris associés à chaque trace. Mais faire ainsi fait qu'il y a une certaine redondance dans le contenu du cache de trace, quand des traces partagent des blocs de base à leur début. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
D'autres caches de traces ne permettent pas de stocker plusieurs traces dont le début est identique, seule une seule trace par adresse de départ est autorisée. Le résultat est que les défauts de cache de trace sont plus fréquents, mais la gestion du cache est plus simple. Il est alors possible de limiter la casse avec des succès de cache partiels. Prenez le cas où le processeur veut lire une trace, mais que le cache a mémorisé une trace similaire, dont seuls les premiers blocs de base correspondent. Dans ce cas, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
La présence d'un cache de traces demande d'utiliser une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. Malheureusement, ces dernières sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
ng26ydw7ay0wfhzdk2wk9svu79m8ejs
745605
745604
2025-06-29T15:59:03Z
Mewtow
31375
/* Le cache de traces */
745605
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D.
Un cache de trace idéal peut mémoriser les deux traces ABC et ABD. Pour le dire autrement, il peut mémoriser des traces différentes, même si leur début est le même. Il fera la différence entre les traces en regardant les branchements pris associés à chaque trace. Mais faire ainsi fait qu'il y a une certaine redondance dans le contenu du cache de trace, quand des traces partagent des blocs de base à leur début. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
D'autres caches de traces ne permettent pas de stocker plusieurs traces dont le début est identique, seule une seule trace par adresse de départ est autorisée. Le résultat est que les défauts de cache de trace sont plus fréquents, mais la gestion du cache est plus simple. Il est alors possible de limiter la casse avec des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire une trace, mais que le cache a mémorisé une trace similaire, dont seuls les premiers blocs de base correspondent. Dans ce cas, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
La présence d'un cache de traces demande d'utiliser une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. Malheureusement, ces dernières sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
0cacb4kstskqqslvn3kyx1gjncihp8t
745606
745605
2025-06-29T16:05:54Z
Mewtow
31375
/* Le cache de traces */
745606
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D.
Un cache de trace idéal peut mémoriser les deux traces ABC et ABD. Pour le dire autrement, il peut mémoriser des traces différentes, même si leur début est le même. Il fera la différence entre les traces en regardant les branchements pris associés à chaque trace. Mais faire ainsi fait qu'il y a une certaine redondance dans le contenu du cache de trace, quand des traces partagent des blocs de base à leur début. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
La présence d'un cache de traces demande d'utiliser une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. Malheureusement, ces dernières sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
7gkaw95f6wx5unuew7350yejbq3e4kg
745607
745606
2025-06-29T16:07:52Z
Mewtow
31375
/* Le cache de traces */
745607
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
La présence d'un cache de traces demande d'utiliser une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. Malheureusement, ces dernières sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
alcm3egbm91lzovl1dmlty7vhosfm2l
745608
745607
2025-06-29T16:10:07Z
Mewtow
31375
/* Le cache de traces */
745608
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, les traces qu'ils stockaient n'étaient pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
La présence d'un cache de traces demande d'utiliser une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. Malheureusement, ces dernières sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
2dk4yyphim9nfub8sn2ls5bdqo1ixaj
745609
745608
2025-06-29T16:11:47Z
Mewtow
31375
/* Le cache de traces */
745609
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
La présence d'un cache de traces demande d'utiliser une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. Malheureusement, ces dernières sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
syj17948nmjcgoeui1iyqe1mopdsac1
745610
745609
2025-06-29T16:12:44Z
Mewtow
31375
/* Le cache de traces */
745610
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner une suite de deux-trois instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
aqm9tudozoibc0oieqd8mi39nn9dyo5
745612
745610
2025-06-29T16:23:41Z
Mewtow
31375
/* La macro-fusion d'instructions */
745612
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les opérandes le permettent. Il est aussi possible de fusionner un calcul d'adresse suivi d'une lecture de l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement.
Cette technique n'est pas exclusive aux processeurs superscalaires, mais elle fonctionne au mieux sur ces processeurs. Sur les processeurs superscalaires, elle se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Par exemple, prenons un processeur double émission. Si deux instructions fusionnables entrent dans les deux décodeurs, ils peuvent communiquer pour détecter que la paire d'instruction est macro-fusionnable et fournir une seule micro-opération en sortie. Sur les processeurs non-superscalaires, la macro-fusion se fait dans la file d'instruction, avant le décodage, pas pendant.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Les instructions sont alignées sur 32 bits, ce qui fait qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits, soit une instruction unique de 16 bits. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
sovprxm0zpb8z3t11jjfmz464r9jce2
745613
745612
2025-06-29T16:24:19Z
Mewtow
31375
/* La macro-fusion d'instructions */
745613
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
===La macro-fusion d'instructions===
La technique de '''macro-fusion''' permet au décodeur de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les opérandes le permettent. Il est aussi possible de fusionner un calcul d'adresse suivi d'une lecture de l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement.
Cette technique n'est pas exclusive aux processeurs superscalaires, mais elle fonctionne au mieux sur ces processeurs. Sur les processeurs superscalaires, elle se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Par exemple, prenons un processeur double émission. Si deux instructions fusionnables entrent dans les deux décodeurs, ils peuvent communiquer pour détecter que la paire d'instruction est macro-fusionnable et fournir une seule micro-opération en sortie. Sur les processeurs non-superscalaires, la macro-fusion se fait dans la file d'instruction, avant le décodage, pas pendant.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
2t1tia4y86fpo3uqvos4ohw0ttaf3oi
745619
745613
2025-06-29T16:49:32Z
Mewtow
31375
/* La macro-fusion d'instructions */
745619
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, mais ce n'est pas fait sur les processeurs x86 modernes, à ma connaissance.
La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Par exemple, prenons un processeur double émission. Si une paire d'instructions adéquate entre dans deux décodeurs, ils peuvent communiquer pour détecter que la paire d'instruction est macro-fusionnable et fournir une seule micro-opération en sortie.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
8e31is0ic1dc33zg9q1p8pvhg46bvn1
745620
745619
2025-06-29T16:50:34Z
Mewtow
31375
/* Les décodeurs d'instructions superscalaires */
745620
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, mais ce n'est pas fait sur les processeurs x86 modernes, à ma connaissance.
La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Par exemple, prenons un processeur double émission. Si une paire d'instructions adéquate entre dans deux décodeurs, ils peuvent communiquer pour détecter que la paire d'instruction est macro-fusionnable et fournir une seule micro-opération en sortie. L'avantage est que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB la fenêtre d'instruction, ou toute autre structure qui stocke les micro-opérations.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Les instructions sont alignées sur 32 bits, ce qui fait qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits, soit une instruction unique de 16 bits. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
502xykqxhwst5i7bvkwz0xa6um8mcfv
745622
745620
2025-06-29T16:55:18Z
Mewtow
31375
/* Les décodeurs d'instructions superscalaires */
745622
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, mais ce n'est pas fait sur les processeurs x86 modernes, à ma connaissance.
La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Par exemple, prenons un processeur double émission. Si une paire d'instructions adéquate entre dans deux décodeurs, ils peuvent communiquer pour détecter que la paire d'instruction est macro-fusionnable et fournir une seule micro-opération en sortie. L'avantage est que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB la fenêtre d'instruction, ou toute autre structure qui stocke les micro-opérations.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
4afux6w3ppygi89tq5h09439piuskx9
745623
745622
2025-06-29T16:59:35Z
Mewtow
31375
/* Les décodeurs d'instructions superscalaires */
745623
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance).
La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Par exemple, prenons un processeur double émission. Si une paire d'instructions adéquate entre dans deux décodeurs, ils peuvent communiquer pour détecter que la paire d'instruction est macro-fusionnable et fournir une seule micro-opération en sortie. L'avantage est que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB la fenêtre d'instruction, ou toute autre structure qui stocke les micro-opérations.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
0spvaydkleveszc15vsb5wq206uhxwx
745624
745623
2025-06-29T17:00:04Z
Mewtow
31375
/* Les décodeurs d'instructions superscalaires */
745624
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Sur les processeurs CISC, la duplication des décodeurs n'est pas exacte. En effet, les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente des décodeurs câblés. Dupliquer naïvement le décodeur demanderait de dupliquer le microcode, ce qui aurait un cout en transistors assez important. Aussi, les processeurs CISC superscalaires ne dupliquent pas le microcode, seulement les décodeurs câblés. Ils disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode qui est utilisé pour les instructions microcodées.
La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible si un veut décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrémement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
q0kgjsf21mlr6v71lgueffb93166uo9
745625
745624
2025-06-29T17:04:35Z
Mewtow
31375
/* Les décodeurs d'instructions superscalaires */
745625
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire a besoin de plusieurs avals, de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Cette intuition correspond à ce que je vais désigner par le terme '''processeur superscalaire homogène''', à savoir un CPU superscalaire où toutes les unités de calcul sont identiques. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances.
Manque de chance, ce cas est l'exception, pour plusieurs raisons. La raison principale est que les unités de calcul d'un processeur superscalaire ne sont pas homogènes, elles ne sont pas identiques. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes; avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse inclus) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission.
Intuitivement, on se dit qu'il faudrait dupliquer toutes ces unités pour obtenir un processeur superscalaire. Par exemple, pour un processeur double émission, on duplique toutes les unités de calcul, et ont utilise deux ports d'émission, chacun relié à une ALU entière, une FPU, une unité de branchement et une unité mémoire. Mais le cout en transistors serait alors trop important. Une autre méthode, bien plus simple, utilise les unités de calcul existantes, en les connectant à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente.
[[File:Amdk6 arch.svg|centre|vignette|upright=2.5|AMD K6.]]
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N 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.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. Mais il y aura une section sur les processeurs AMD à la fin.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les microarchitectures x86 d'AMD===
Les architectures AMD ont beaucoup évoluées avec le temps. La toute première était l''''architecture K5''', qui disposait de deux ALU entières, d'une FPU, de deux unités LOAD/STORE pour les accès mémoire et d'une unité de branchement. Elle utilisait une station de réservation par unité de calcul (sauf pour les unités mémoire, qui partagent une seule station de réservation). Elle était capable de décoder 2 instructions différentes par cycle, mais pouvait renommer 4 micro-opérations par cycle. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:AMDK5Diagram.png|centre|vignette|upright=3|AMDK5 Diagramme.]]
L''''architecture K6''' a ajouté une unité SIMD, sans compter que les deux unités LOAD/STORE sont séparées en une unité LOAD pour les lectures et une unité STORE pour les écritures.
Le K6 contient un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus. Les décodeurs sont capables de décoder une ou deux instructions, en 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opération, les micro-opérations manquantes sont remplies par des NOPs.
L'AMD K6 a remplacé les stations de réservation distribuées pour une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
[[File:Amdk6 arch.svg|centre|vignette|upright=3|Microarchitecture K6 d'AMD.]]
L''''architecture K7''' des processeurs Athlon que décodait trois instructions par cycle, soit une de plus que les architectures précédentes. Elle avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers.
Il y avait trois unités de calcul entières, chacun avec son propre port d'émission, et un circuit multiplieur relié sur le premier port d'émission entier. Les unités de calcul entières peuvent aussi être utilisées comme unités de calcul d'adresse. Niveau unités flottantes, le processeur avait une unité d'addition flottante, une unité de multiplication flottante, et une autre unité pour le reste.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
La microarchitecture suivante, nommée K8, a elle-même été suivie par l'architecture K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 ont les mêmes ALU, mais a une organisation différente pour les stations de réservation. Premièrement, le processeur utilise le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
f96rpkz100el8m7hbqm7dpbq6wulvtd
Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions
0
79799
745611
744081
2025-06-29T16:19:27Z
Mewtow
31375
/* La macro-fusion */
745611
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===La macro-fusion===
La présence d'une file d'instruction permet d'ajouter une optimisation très importante au processeur, qui ne seraient pas possibles sans elle. L'une d'entre elle est la '''macro-fusion''', une technique qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les opérandes le permettent. Il est aussi possible de fusionner un calcul d'adresse suivi d'une lecture de l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires, mais elle fonctionne au mieux sur ces processeurs.
La macro-fusion est effectuée pendant le décodage, en décodant des instructions dans le tampon d'instruction. Le décodeur reconnait que plusieurs instructions dans le tampon d'instruction peuvent être fusionnées et fournit en sortie la micro-opération équivalent. L'avantage de cette technique est que le chemin de données est utilisé plus efficacement. Notons que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB et dans les différentes mémoires intégrées aux processeur, qui stocke les micro-opérations.
La technique est parfois couplée à un circuit de prédiction, qui détermine si une série d'instruction à fusionner va apparaitre sous peu. L'idée est que dans certains cas, le tampon d'instruction contient le début d'une suite d'instruction combinables. Suivant ce qui sera chargé après, la macro-fusion pourra se faire, ou non. Mais le processeur ne sait pas exactement quelles seront les instructions chargées juste après et il ne sait pas si la macro-fusion aura lieu. Dans ce cas, il peut utiliser un circuit de prédiction de macro-fusion, qui essaye de prédire si les instructions chargées sous peu autoriseront une macro-fusion ou non. Si c'est le cas, les instructions potentiellement fusionnables, le début de la série macro-fusionnable, est mise en attente dans le tampon d'instruction, en attendant les futures instructions.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===La micro-fusion===
La présence d'une file de micro-opération permet d'effectuer une optimisation appelée la '''micro-fusion'''. Elle remplace deux micro-opérations simples consécutives en une seule micro-opération complexe équivalente. Par exemple, sur certains processeurs, le chemin de données est capable d'effectuer une lecture/écriture en adressage base+index en une seule micro-opération. Mais le jeu d'instruction est une architecture Load-store où le calcul d'adresse et l'accès mémoire se font en deux instructions séparées. Dans ce cas, la micro-fusion peut faire son travail et fusionner le calcul d'adresse avec l'accès mémoire.
C'est une optimisation compatible avec la macro-fusion, les deux se ressemblant beaucoup. On peut se demander pourquoi les deux existent ensemble. La raison est historique, et tient au design des processeurs x86. Les premiers processeurs x86 avaient un chemin de données simple, avec les décodeurs qui allaient avec. Sur de tels processeurs, le calcul d'adresse et l'accès mémoire étaient séparés en deux instructions. Puis, avec l'évolution de la technologie, le chemin de données est devenu capable de faire les deux en une seule micro-opération. Pour compenser cela, Intel et AMD auraient pu changer leurs décodeurs en profondeur. A la place, ils ont préféré utiliser la technique de la micro-fusion, par simplicité.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
ambro5xzo90y7vfupclevwhoershdet
745614
745611
2025-06-29T16:24:51Z
Mewtow
31375
/* La macro-fusion */
745614
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===La macro-fusion===
La présence d'une file d'instruction permet d'ajouter une optimisation très importante au processeur, qui ne seraient pas possibles sans elle. L'une d'entre elle est la '''macro-fusion''', une technique qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les opérandes le permettent. Il est aussi possible de fusionner un calcul d'adresse suivi d'une lecture de l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires, mais elle fonctionne au mieux sur ces processeurs.
La macro-fusion est effectuée avant ou pendant le décodage, dans le tampon d'instruction. Le décodeur reconnait que plusieurs instructions dans le tampon d'instruction peuvent être fusionnées et fournit en sortie la micro-opération équivalent. L'avantage de cette technique est que le chemin de données est utilisé plus efficacement. Notons que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB et dans les différentes mémoires intégrées aux processeur, qui stocke les micro-opérations.
La technique est parfois couplée à un circuit de prédiction, qui détermine si une série d'instruction à fusionner va apparaitre sous peu. L'idée est que dans certains cas, le tampon d'instruction contient le début d'une suite d'instruction combinables. Suivant ce qui sera chargé après, la macro-fusion pourra se faire, ou non. Mais le processeur ne sait pas exactement quelles seront les instructions chargées juste après et il ne sait pas si la macro-fusion aura lieu. Dans ce cas, il peut utiliser un circuit de prédiction de macro-fusion, qui essaye de prédire si les instructions chargées sous peu autoriseront une macro-fusion ou non. Si c'est le cas, les instructions potentiellement fusionnables, le début de la série macro-fusionnable, est mise en attente dans le tampon d'instruction, en attendant les futures instructions.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Les instructions sont alignées sur 32 bits, ce qui fait qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits, soit une instruction unique de 16 bits. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===La micro-fusion===
La présence d'une file de micro-opération permet d'effectuer une optimisation appelée la '''micro-fusion'''. Elle remplace deux micro-opérations simples consécutives en une seule micro-opération complexe équivalente. Par exemple, sur certains processeurs, le chemin de données est capable d'effectuer une lecture/écriture en adressage base+index en une seule micro-opération. Mais le jeu d'instruction est une architecture Load-store où le calcul d'adresse et l'accès mémoire se font en deux instructions séparées. Dans ce cas, la micro-fusion peut faire son travail et fusionner le calcul d'adresse avec l'accès mémoire.
C'est une optimisation compatible avec la macro-fusion, les deux se ressemblant beaucoup. On peut se demander pourquoi les deux existent ensemble. La raison est historique, et tient au design des processeurs x86. Les premiers processeurs x86 avaient un chemin de données simple, avec les décodeurs qui allaient avec. Sur de tels processeurs, le calcul d'adresse et l'accès mémoire étaient séparés en deux instructions. Puis, avec l'évolution de la technologie, le chemin de données est devenu capable de faire les deux en une seule micro-opération. Pour compenser cela, Intel et AMD auraient pu changer leurs décodeurs en profondeur. A la place, ils ont préféré utiliser la technique de la micro-fusion, par simplicité.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
db4d6etizuasw536vi8ft3rz9oex68j
745615
745614
2025-06-29T16:25:20Z
Mewtow
31375
/* La macro-fusion */
745615
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===La macro-fusion===
La présence d'une file d'instruction permet d'ajouter une optimisation très importante au processeur, qui ne seraient pas possibles sans elle. L'une d'entre elle est la '''macro-fusion''', une technique qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les opérandes le permettent. Il est aussi possible de fusionner un calcul d'adresse suivi d'une lecture de l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires, mais elle fonctionne au mieux sur ces processeurs.
La macro-fusion est effectuée avant ou pendant le décodage, dans le tampon d'instruction. Le décodeur reconnait que plusieurs instructions dans le tampon d'instruction peuvent être fusionnées et fournit en sortie la micro-opération équivalent. L'avantage de cette technique est que le chemin de données est utilisé plus efficacement. Notons que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB et dans les différentes mémoires intégrées aux processeur, qui stocke les micro-opérations.
La technique est parfois couplée à un circuit de prédiction, qui détermine si une série d'instruction à fusionner va apparaitre sous peu. L'idée est que dans certains cas, le tampon d'instruction contient le début d'une suite d'instruction combinables. Suivant ce qui sera chargé après, la macro-fusion pourra se faire, ou non. Mais le processeur ne sait pas exactement quelles seront les instructions chargées juste après et il ne sait pas si la macro-fusion aura lieu. Dans ce cas, il peut utiliser un circuit de prédiction de macro-fusion, qui essaye de prédire si les instructions chargées sous peu autoriseront une macro-fusion ou non. Si c'est le cas, les instructions potentiellement fusionnables, le début de la série macro-fusionnable, est mise en attente dans le tampon d'instruction, en attendant les futures instructions.
La macro-fusion peut aussi se faire dans le décodeur d'instruction. Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Les instructions sont alignées sur 32 bits, ce qui fait qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits, soit une instruction unique de 16 bits. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===La micro-fusion===
La présence d'une file de micro-opération permet d'effectuer une optimisation appelée la '''micro-fusion'''. Elle remplace deux micro-opérations simples consécutives en une seule micro-opération complexe équivalente. Par exemple, sur certains processeurs, le chemin de données est capable d'effectuer une lecture/écriture en adressage base+index en une seule micro-opération. Mais le jeu d'instruction est une architecture Load-store où le calcul d'adresse et l'accès mémoire se font en deux instructions séparées. Dans ce cas, la micro-fusion peut faire son travail et fusionner le calcul d'adresse avec l'accès mémoire.
C'est une optimisation compatible avec la macro-fusion, les deux se ressemblant beaucoup. On peut se demander pourquoi les deux existent ensemble. La raison est historique, et tient au design des processeurs x86. Les premiers processeurs x86 avaient un chemin de données simple, avec les décodeurs qui allaient avec. Sur de tels processeurs, le calcul d'adresse et l'accès mémoire étaient séparés en deux instructions. Puis, avec l'évolution de la technologie, le chemin de données est devenu capable de faire les deux en une seule micro-opération. Pour compenser cela, Intel et AMD auraient pu changer leurs décodeurs en profondeur. A la place, ils ont préféré utiliser la technique de la micro-fusion, par simplicité.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
6xcvv6u7aogyu3r1gn4xfbujt0je9de
745616
745615
2025-06-29T16:26:24Z
Mewtow
31375
/* La macro-fusion */
745616
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===La micro-fusion===
La présence d'une file de micro-opération permet d'effectuer une optimisation appelée la '''micro-fusion'''. Elle remplace deux micro-opérations simples consécutives en une seule micro-opération complexe équivalente. Par exemple, sur certains processeurs, le chemin de données est capable d'effectuer une lecture/écriture en adressage base+index en une seule micro-opération. Mais le jeu d'instruction est une architecture Load-store où le calcul d'adresse et l'accès mémoire se font en deux instructions séparées. Dans ce cas, la micro-fusion peut faire son travail et fusionner le calcul d'adresse avec l'accès mémoire.
C'est une optimisation compatible avec la macro-fusion, les deux se ressemblant beaucoup. On peut se demander pourquoi les deux existent ensemble. La raison est historique, et tient au design des processeurs x86. Les premiers processeurs x86 avaient un chemin de données simple, avec les décodeurs qui allaient avec. Sur de tels processeurs, le calcul d'adresse et l'accès mémoire étaient séparés en deux instructions. Puis, avec l'évolution de la technologie, le chemin de données est devenu capable de faire les deux en une seule micro-opération. Pour compenser cela, Intel et AMD auraient pu changer leurs décodeurs en profondeur. A la place, ils ont préféré utiliser la technique de la micro-fusion, par simplicité.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
8pl0sm5ps1gpsdg2opbelunm8b2fujs
745617
745616
2025-06-29T16:26:33Z
Mewtow
31375
/* Le Fetch Directed Instruction Prefetching */
745617
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===La micro-fusion===
La présence d'une file de micro-opération permet d'effectuer une optimisation appelée la '''micro-fusion'''. Elle remplace deux micro-opérations simples consécutives en une seule micro-opération complexe équivalente. Par exemple, sur certains processeurs, le chemin de données est capable d'effectuer une lecture/écriture en adressage base+index en une seule micro-opération. Mais le jeu d'instruction est une architecture Load-store où le calcul d'adresse et l'accès mémoire se font en deux instructions séparées. Dans ce cas, la micro-fusion peut faire son travail et fusionner le calcul d'adresse avec l'accès mémoire.
C'est une optimisation compatible avec la macro-fusion, les deux se ressemblant beaucoup. On peut se demander pourquoi les deux existent ensemble. La raison est historique, et tient au design des processeurs x86. Les premiers processeurs x86 avaient un chemin de données simple, avec les décodeurs qui allaient avec. Sur de tels processeurs, le calcul d'adresse et l'accès mémoire étaient séparés en deux instructions. Puis, avec l'évolution de la technologie, le chemin de données est devenu capable de faire les deux en une seule micro-opération. Pour compenser cela, Intel et AMD auraient pu changer leurs décodeurs en profondeur. A la place, ils ont préféré utiliser la technique de la micro-fusion, par simplicité.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==La macro-fusion==
La '''macro-fusion''' permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les opérandes le permettent. Il est aussi possible de fusionner un calcul d'adresse suivi d'une lecture de l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires, mais elle fonctionne au mieux sur ces processeurs.
La macro-fusion est effectuée avant ou pendant le décodage, dans le tampon d'instruction. Le décodeur reconnait que plusieurs instructions dans le tampon d'instruction peuvent être fusionnées et fournit en sortie la micro-opération équivalent. L'avantage de cette technique est que le chemin de données est utilisé plus efficacement. Notons que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB et dans les différentes mémoires intégrées aux processeur, qui stocke les micro-opérations.
La technique est parfois couplée à un circuit de prédiction, qui détermine si une série d'instruction à fusionner va apparaitre sous peu. L'idée est que dans certains cas, le tampon d'instruction contient le début d'une suite d'instruction combinables. Suivant ce qui sera chargé après, la macro-fusion pourra se faire, ou non. Mais le processeur ne sait pas exactement quelles seront les instructions chargées juste après et il ne sait pas si la macro-fusion aura lieu. Dans ce cas, il peut utiliser un circuit de prédiction de macro-fusion, qui essaye de prédire si les instructions chargées sous peu autoriseront une macro-fusion ou non. Si c'est le cas, les instructions potentiellement fusionnables, le début de la série macro-fusionnable, est mise en attente dans le tampon d'instruction, en attendant les futures instructions.
La macro-fusion peut aussi se faire dans le décodeur d'instruction. Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Les instructions sont alignées sur 32 bits, ce qui fait qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits, soit une instruction unique de 16 bits. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
6asz7d01ne5o5og8kvdfnfct41f0291
745618
745617
2025-06-29T16:27:03Z
Mewtow
31375
/* La macro-fusion */
745618
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===La micro-fusion===
La présence d'une file de micro-opération permet d'effectuer une optimisation appelée la '''micro-fusion'''. Elle remplace deux micro-opérations simples consécutives en une seule micro-opération complexe équivalente. Par exemple, sur certains processeurs, le chemin de données est capable d'effectuer une lecture/écriture en adressage base+index en une seule micro-opération. Mais le jeu d'instruction est une architecture Load-store où le calcul d'adresse et l'accès mémoire se font en deux instructions séparées. Dans ce cas, la micro-fusion peut faire son travail et fusionner le calcul d'adresse avec l'accès mémoire.
C'est une optimisation compatible avec la macro-fusion, les deux se ressemblant beaucoup. On peut se demander pourquoi les deux existent ensemble. La raison est historique, et tient au design des processeurs x86. Les premiers processeurs x86 avaient un chemin de données simple, avec les décodeurs qui allaient avec. Sur de tels processeurs, le calcul d'adresse et l'accès mémoire étaient séparés en deux instructions. Puis, avec l'évolution de la technologie, le chemin de données est devenu capable de faire les deux en une seule micro-opération. Pour compenser cela, Intel et AMD auraient pu changer leurs décodeurs en profondeur. A la place, ils ont préféré utiliser la technique de la micro-fusion, par simplicité.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==La macro-fusion==
La '''macro-fusion''' permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD (''multiply and add''), si les opérandes le permettent. Il est aussi possible de fusionner un calcul d'adresse suivi d'une lecture de l'adresse calculée en une seule micro-opération d'accès mémoire. Ou encore, fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Cette technique n'est pas exclusive aux processeurs superscalaires, mais elle fonctionne au mieux sur ces processeurs.
La macro-fusion est effectuée avant ou pendant le décodage, soit dans le tampon d'instruction, soit dans un circuit séparé. La technique la plus simple reconnait que plusieurs instructions dans le tampon d'instruction peuvent être fusionnées et fournit en sortie la micro-opération équivalent. L'avantage de cette technique est que le chemin de données est utilisé plus efficacement. Notons que la macro-fusion diminue le nombre d'instructions à stocker dans le ROB et dans les différentes mémoires intégrées aux processeur, qui stocke les micro-opérations.
La technique est parfois couplée à un circuit de prédiction, qui détermine si une série d'instruction à fusionner va apparaitre sous peu. L'idée est que dans certains cas, le tampon d'instruction contient le début d'une suite d'instruction combinables. Suivant ce qui sera chargé après, la macro-fusion pourra se faire, ou non. Mais le processeur ne sait pas exactement quelles seront les instructions chargées juste après et il ne sait pas si la macro-fusion aura lieu. Dans ce cas, il peut utiliser un circuit de prédiction de macro-fusion, qui essaye de prédire si les instructions chargées sous peu autoriseront une macro-fusion ou non. Si c'est le cas, les instructions potentiellement fusionnables, le début de la série macro-fusionnable, est mise en attente dans le tampon d'instruction, en attendant les futures instructions.
La macro-fusion peut aussi se faire dans le décodeur d'instruction. Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Les instructions sont alignées sur 32 bits, ce qui fait qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits, soit une instruction unique de 16 bits. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
[[File:Décodage des instructions à longueur variable, exemple.png|centre|vignette|upright=2|Décodage des instructions à longueur variable dans l'exemple du RISC-V, où les instructions font 32 ou 16 bits et sont alignées.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
ff3yx6g5v6jsd6mzznpu5q9gjti73al
745621
745618
2025-06-29T16:50:43Z
Mewtow
31375
/* La macro-fusion */
745621
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===La micro-fusion===
La présence d'une file de micro-opération permet d'effectuer une optimisation appelée la '''micro-fusion'''. Elle remplace deux micro-opérations simples consécutives en une seule micro-opération complexe équivalente. Par exemple, sur certains processeurs, le chemin de données est capable d'effectuer une lecture/écriture en adressage base+index en une seule micro-opération. Mais le jeu d'instruction est une architecture Load-store où le calcul d'adresse et l'accès mémoire se font en deux instructions séparées. Dans ce cas, la micro-fusion peut faire son travail et fusionner le calcul d'adresse avec l'accès mémoire.
C'est une optimisation compatible avec la macro-fusion, les deux se ressemblant beaucoup. On peut se demander pourquoi les deux existent ensemble. La raison est historique, et tient au design des processeurs x86. Les premiers processeurs x86 avaient un chemin de données simple, avec les décodeurs qui allaient avec. Sur de tels processeurs, le calcul d'adresse et l'accès mémoire étaient séparés en deux instructions. Puis, avec l'évolution de la technologie, le chemin de données est devenu capable de faire les deux en une seule micro-opération. Pour compenser cela, Intel et AMD auraient pu changer leurs décodeurs en profondeur. A la place, ils ont préféré utiliser la technique de la micro-fusion, par simplicité.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
8pl0sm5ps1gpsdg2opbelunm8b2fujs
745626
745621
2025-06-29T20:19:13Z
Mewtow
31375
/* La micro-fusion */
745626
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
steulq4b4q2dqseivl9ojyhglrkm23u